From 89202ce64969242bc9f6846ba284fccae346118e Mon Sep 17 00:00:00 2001 From: Erik Franz Date: Thu, 23 Mar 2023 15:15:17 +0100 Subject: [PATCH] added code --- .gitignore | 9 + NGT_env.txt | 120 + SDFDiff_cameras.json | 647 +++ common_setups.py | 1612 ++++++ compile.py | 83 + configs/setup_density.json | 1346 +++++ configs/setup_velocity.json | 1292 +++++ eval_runs.py | 489 ++ images/Framework.PNG | Bin 0 -> 200269 bytes images/SF_targets.PNG | Bin 0 -> 442991 bytes phitest/render/__init__.py | 12 + phitest/render/camera.py | 464 ++ phitest/render/cuda/ops_loader.py | 120 + phitest/render/cuda/src/advect.cc | 481 ++ phitest/render/cuda/src/advect.cu.cc | 389 ++ phitest/render/cuda/src/advect.hpp | 25 + phitest/render/cuda/src/blending.hpp | 248 + phitest/render/cuda/src/blending_settings.hpp | 15 + phitest/render/cuda/src/bounds_checks.hpp | 45 + .../src/cuda-samples/Common/helper_cuda.h | 967 ++++ .../src/cuda-samples/Common/helper_math.h | 1469 +++++ .../src/cuda-samples/Common/helper_string.h | 368 ++ phitest/render/cuda/src/dimensions_v2.hpp | 53 + phitest/render/cuda/src/kernel_setup.hpp | 203 + phitest/render/cuda/src/raymarch_grid.cc | 539 ++ phitest/render/cuda/src/raymarch_grid.cu.cc | 748 +++ phitest/render/cuda/src/raymarch_grid.hpp | 58 + phitest/render/cuda/src/reduce_blend.cc | 261 + phitest/render/cuda/src/reduce_blend.cu.cc | 245 + phitest/render/cuda/src/render_errors.hpp | 61 + phitest/render/cuda/src/resample_grid.cc | 1221 +++++ phitest/render/cuda/src/resample_grid.cu.cc | 1788 ++++++ phitest/render/cuda/src/resample_grid.hpp | 42 + phitest/render/cuda/src/sampling.hpp | 1065 ++++ phitest/render/cuda/src/sampling_settings.hpp | 74 + .../render/cuda/src/sampling_settings_v2.hpp | 16 + phitest/render/cuda/src/sampling_v2.hpp | 834 +++ phitest/render/cuda/src/transformations.hpp | 154 + .../render/cuda/src/transformations_v2.hpp | 165 + phitest/render/cuda/src/vector_io.hpp | 75 + phitest/render/cuda/src/vector_io.inl | 363 ++ phitest/render/cuda/src/vectormath.hpp | 613 +++ phitest/render/cuda/src/vectormath_helper.hpp | 304 ++ phitest/render/data_structures.py | 2668 +++++++++ phitest/render/discriminator.py | 2 + phitest/render/generator_models.py | 2373 ++++++++ phitest/render/image_writer.py | 59 + phitest/render/lighting.py | 194 + phitest/render/neural_data_structures.py | 3730 +++++++++++++ phitest/render/optimization.py | 2697 +++++++++ phitest/render/profiling.py | 393 ++ phitest/render/render_helper.py | 116 + phitest/render/renderer.py | 1130 ++++ phitest/render/serialization.py | 35 + phitest/render/transform.py | 428 ++ phitest/render/vector.py | 555 ++ reconstruct_sequence.py | 4809 +++++++++++++++++ scalaFlow_cameras.json | 173 + 58 files changed, 38445 insertions(+) create mode 100644 NGT_env.txt create mode 100644 SDFDiff_cameras.json create mode 100644 common_setups.py create mode 100644 compile.py create mode 100644 configs/setup_density.json create mode 100644 configs/setup_velocity.json create mode 100644 eval_runs.py create mode 100644 images/Framework.PNG create mode 100644 images/SF_targets.PNG create mode 100644 phitest/render/__init__.py create mode 100644 phitest/render/camera.py create mode 100644 phitest/render/cuda/ops_loader.py create mode 100644 phitest/render/cuda/src/advect.cc create mode 100644 phitest/render/cuda/src/advect.cu.cc create mode 100644 phitest/render/cuda/src/advect.hpp create mode 100644 phitest/render/cuda/src/blending.hpp create mode 100644 phitest/render/cuda/src/blending_settings.hpp create mode 100644 phitest/render/cuda/src/bounds_checks.hpp create mode 100644 phitest/render/cuda/src/cuda-samples/Common/helper_cuda.h create mode 100644 phitest/render/cuda/src/cuda-samples/Common/helper_math.h create mode 100644 phitest/render/cuda/src/cuda-samples/Common/helper_string.h create mode 100644 phitest/render/cuda/src/dimensions_v2.hpp create mode 100644 phitest/render/cuda/src/kernel_setup.hpp create mode 100644 phitest/render/cuda/src/raymarch_grid.cc create mode 100644 phitest/render/cuda/src/raymarch_grid.cu.cc create mode 100644 phitest/render/cuda/src/raymarch_grid.hpp create mode 100644 phitest/render/cuda/src/reduce_blend.cc create mode 100644 phitest/render/cuda/src/reduce_blend.cu.cc create mode 100644 phitest/render/cuda/src/render_errors.hpp create mode 100644 phitest/render/cuda/src/resample_grid.cc create mode 100644 phitest/render/cuda/src/resample_grid.cu.cc create mode 100644 phitest/render/cuda/src/resample_grid.hpp create mode 100644 phitest/render/cuda/src/sampling.hpp create mode 100644 phitest/render/cuda/src/sampling_settings.hpp create mode 100644 phitest/render/cuda/src/sampling_settings_v2.hpp create mode 100644 phitest/render/cuda/src/sampling_v2.hpp create mode 100644 phitest/render/cuda/src/transformations.hpp create mode 100644 phitest/render/cuda/src/transformations_v2.hpp create mode 100644 phitest/render/cuda/src/vector_io.hpp create mode 100644 phitest/render/cuda/src/vector_io.inl create mode 100644 phitest/render/cuda/src/vectormath.hpp create mode 100644 phitest/render/cuda/src/vectormath_helper.hpp create mode 100644 phitest/render/data_structures.py create mode 100644 phitest/render/discriminator.py create mode 100644 phitest/render/generator_models.py create mode 100644 phitest/render/image_writer.py create mode 100644 phitest/render/lighting.py create mode 100644 phitest/render/neural_data_structures.py create mode 100644 phitest/render/optimization.py create mode 100644 phitest/render/profiling.py create mode 100644 phitest/render/render_helper.py create mode 100644 phitest/render/renderer.py create mode 100644 phitest/render/serialization.py create mode 100644 phitest/render/transform.py create mode 100644 phitest/render/vector.py create mode 100644 reconstruct_sequence.py create mode 100644 scalaFlow_cameras.json diff --git a/.gitignore b/.gitignore index b6e4761..273d518 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ __pycache__/ # C extensions *.so +*.o # Distribution / packaging .Python @@ -127,3 +128,11 @@ dmypy.json # Pyre type checker .pyre/ + +# data +runs/ +data/ +eval/ +*.png +*.exr +*.npz diff --git a/NGT_env.txt b/NGT_env.txt new file mode 100644 index 0000000..384eb24 --- /dev/null +++ b/NGT_env.txt @@ -0,0 +1,120 @@ +# This file may be used to create an environment using: +# $ conda create --name --file +# platform: linux-64 +@EXPLICIT +https://repo.anaconda.com/pkgs/main/linux-64/_libgcc_mutex-0.1-main.conda +https://repo.anaconda.com/pkgs/main/linux-64/_tflow_select-2.1.0-gpu.conda +https://repo.anaconda.com/pkgs/main/linux-64/blas-1.0-mkl.conda +https://repo.anaconda.com/pkgs/main/linux-64/ca-certificates-2023.01.10-h06a4308_0.conda +https://repo.anaconda.com/pkgs/main/linux-64/cudatoolkit-9.2-0.conda +https://repo.anaconda.com/pkgs/main/linux-64/libgfortran4-7.5.0-ha8ba4b0_17.conda +https://repo.anaconda.com/pkgs/main/linux-64/libstdcxx-ng-11.2.0-h1234567_1.conda +https://repo.anaconda.com/pkgs/main/linux-64/cudnn-7.6.5-cuda9.2_0.conda +https://repo.anaconda.com/pkgs/main/linux-64/cupti-9.2.148-0.conda +https://repo.anaconda.com/pkgs/main/linux-64/libgfortran-ng-7.5.0-ha8ba4b0_17.conda +https://repo.anaconda.com/pkgs/main/linux-64/libgomp-11.2.0-h1234567_1.conda +https://repo.anaconda.com/pkgs/main/linux-64/_openmp_mutex-5.1-1_gnu.conda +https://repo.anaconda.com/pkgs/main/linux-64/libgcc-ng-11.2.0-h1234567_1.conda +https://repo.anaconda.com/pkgs/main/linux-64/c-ares-1.19.0-h5eee18b_0.conda +https://repo.anaconda.com/pkgs/main/linux-64/expat-2.4.9-h6a678d5_0.conda +https://repo.anaconda.com/pkgs/main/linux-64/icu-58.2-he6710b0_3.conda +https://repo.anaconda.com/pkgs/main/linux-64/jpeg-9e-h5eee18b_1.conda +https://repo.anaconda.com/pkgs/main/linux-64/lerc-3.0-h295c915_0.conda +https://repo.anaconda.com/pkgs/main/linux-64/libdeflate-1.17-h5eee18b_0.conda +https://repo.continuum.io/pkgs/main/linux-64/libffi-3.2.1-hf484d3e_1007.tar.bz2 +https://repo.anaconda.com/pkgs/main/linux-64/libuuid-1.41.5-h5eee18b_0.conda +https://repo.anaconda.com/pkgs/main/linux-64/libwebp-base-1.2.4-h5eee18b_1.conda +https://repo.anaconda.com/pkgs/main/linux-64/libxcb-1.15-h7f8727e_0.conda +https://repo.anaconda.com/pkgs/main/linux-64/lz4-c-1.9.4-h6a678d5_0.conda +https://repo.anaconda.com/pkgs/main/linux-64/ncurses-6.4-h6a678d5_0.conda +https://repo.continuum.io/pkgs/main/linux-64/openssl-1.1.1t-h7f8727e_0.tar.bz2 +https://repo.anaconda.com/pkgs/main/linux-64/pcre-8.45-h295c915_0.conda +https://repo.anaconda.com/pkgs/main/linux-64/xz-5.2.10-h5eee18b_1.conda +https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h7f98852_2.tar.bz2 +https://repo.anaconda.com/pkgs/main/linux-64/zlib-1.2.13-h5eee18b_0.conda +https://repo.anaconda.com/pkgs/main/linux-64/glib-2.63.1-h5a9c865_0.conda +https://repo.anaconda.com/pkgs/main/linux-64/hdf5-1.10.6-hb1b8bf9_0.conda +https://repo.anaconda.com/pkgs/main/linux-64/intel-openmp-2022.1.0-h9e868ea_3769.conda +https://repo.anaconda.com/pkgs/main/linux-64/libedit-3.1.20221030-h5eee18b_0.conda +https://repo.anaconda.com/pkgs/main/linux-64/libpng-1.6.39-h5eee18b_0.conda +https://repo.anaconda.com/pkgs/main/linux-64/libprotobuf-3.17.2-h4ff587b_1.conda +https://repo.anaconda.com/pkgs/main/linux-64/libxml2-2.9.14-h74e7548_0.conda +https://repo.continuum.io/pkgs/main/linux-64/readline-7.0-h7b6447c_5.tar.bz2 +https://repo.anaconda.com/pkgs/main/linux-64/tk-8.6.12-h1ccaba5_0.conda +https://repo.anaconda.com/pkgs/main/linux-64/zstd-1.5.2-ha4553b6_0.conda +https://repo.anaconda.com/pkgs/main/linux-64/dbus-1.13.18-hb2f20db_0.conda +https://repo.anaconda.com/pkgs/main/linux-64/freetype-2.12.1-h4a9f257_0.conda +https://repo.anaconda.com/pkgs/main/linux-64/gstreamer-1.14.0-hb453b48_1.conda +https://repo.anaconda.com/pkgs/main/linux-64/libtiff-4.5.0-h6a678d5_2.conda +https://repo.anaconda.com/pkgs/main/linux-64/mkl-2020.2-256.conda +https://repo.continuum.io/pkgs/main/linux-64/sqlite-3.33.0-h62c20be_0.tar.bz2 +https://repo.anaconda.com/pkgs/main/linux-64/fontconfig-2.14.1-h52c9d5c_1.conda +https://repo.anaconda.com/pkgs/main/linux-64/gst-plugins-base-1.14.0-hbbd80ab_1.conda +https://repo.anaconda.com/pkgs/main/linux-64/lcms2-2.12-h3be6417_0.conda +https://repo.anaconda.com/pkgs/main/linux-64/openjpeg-2.4.0-h3ad879b_0.conda +https://repo.continuum.io/pkgs/main/linux-64/python-3.6.9-h265db76_0.tar.bz2 +https://repo.anaconda.com/pkgs/main/linux-64/astor-0.8.1-py36h06a4308_0.conda +https://repo.continuum.io/pkgs/main/linux-64/certifi-2021.5.30-py36h06a4308_0.tar.bz2 +https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-2.1.1-pyhd8ed1ab_0.tar.bz2 +https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.5-pyhd8ed1ab_0.tar.bz2 +https://repo.anaconda.com/pkgs/main/linux-64/coverage-5.5-py36h27cfd23_2.conda +https://repo.anaconda.com/pkgs/main/noarch/cycler-0.11.0-pyhd3eb1b0_0.conda +https://repo.anaconda.com/pkgs/main/linux-64/cython-0.29.24-py36h295c915_0.conda +https://repo.anaconda.com/pkgs/main/noarch/dataclasses-0.8-pyh4f3eec9_6.conda +https://repo.anaconda.com/pkgs/main/noarch/gast-0.5.3-pyhd3eb1b0_0.conda +https://conda.anaconda.org/conda-forge/noarch/idna-3.4-pyhd8ed1ab_0.tar.bz2 +https://repo.anaconda.com/pkgs/main/linux-64/kiwisolver-1.3.1-py36h2531618_0.tar.bz2 +https://repo.anaconda.com/pkgs/main/linux-64/olefile-0.46-py36_0.conda +https://repo.anaconda.com/pkgs/main/linux-64/psutil-5.8.0-py36h27cfd23_1.conda +https://conda.anaconda.org/conda-forge/noarch/pycparser-2.21-pyhd8ed1ab_0.tar.bz2 +https://repo.anaconda.com/pkgs/main/noarch/pyparsing-3.0.4-pyhd3eb1b0_0.conda +https://conda.anaconda.org/conda-forge/linux-64/python_abi-3.6-2_cp36m.tar.bz2 +https://repo.anaconda.com/pkgs/main/linux-64/qt-5.9.7-h5867ecd_1.conda +https://repo.anaconda.com/pkgs/main/linux-64/sip-4.19.8-py36hf484d3e_0.conda +https://repo.anaconda.com/pkgs/main/noarch/six-1.16.0-pyhd3eb1b0_1.conda +https://repo.anaconda.com/pkgs/main/linux-64/termcolor-1.1.0-py36h06a4308_1.conda +https://repo.anaconda.com/pkgs/main/linux-64/tornado-6.1-py36h27cfd23_0.conda +https://repo.anaconda.com/pkgs/main/noarch/typing_extensions-4.1.1-pyh06a4308_0.conda +https://repo.anaconda.com/pkgs/main/noarch/wheel-0.37.1-pyhd3eb1b0_0.conda +https://repo.anaconda.com/pkgs/main/noarch/zipp-3.6.0-pyhd3eb1b0_0.conda +https://repo.anaconda.com/pkgs/main/noarch/absl-py-0.15.0-pyhd3eb1b0_0.conda +https://conda.anaconda.org/conda-forge/linux-64/cffi-1.14.4-py36h211aa47_0.tar.bz2 +https://repo.anaconda.com/pkgs/main/linux-64/importlib-metadata-4.8.1-py36h06a4308_0.conda +https://repo.anaconda.com/pkgs/main/linux-64/mkl-service-2.3.0-py36he8ac12f_0.conda +https://repo.anaconda.com/pkgs/main/linux-64/pillow-8.3.1-py36h2c7a002_0.conda +https://conda.anaconda.org/conda-forge/linux-64/pycosat-0.6.3-py36h8f6f2f9_1006.tar.bz2 +https://repo.anaconda.com/pkgs/main/linux-64/pyqt-5.9.2-py36h05f1152_2.conda +https://conda.anaconda.org/conda-forge/linux-64/pysocks-1.7.1-py36h5fab9bb_3.tar.bz2 +https://repo.anaconda.com/pkgs/main/noarch/python-dateutil-2.8.2-pyhd3eb1b0_0.conda +https://conda.anaconda.org/conda-forge/linux-64/ruamel_yaml-0.15.80-py36h8f6f2f9_1004.tar.bz2 +https://repo.continuum.io/pkgs/main/linux-64/setuptools-58.0.4-py36h06a4308_0.tar.bz2 +https://conda.anaconda.org/conda-forge/noarch/tqdm-4.65.0-pyhd8ed1ab_0.conda +https://repo.anaconda.com/pkgs/main/noarch/werkzeug-2.0.3-pyhd3eb1b0_0.conda +https://conda.anaconda.org/conda-forge/linux-64/brotlipy-0.7.0-py36h8f6f2f9_1001.tar.bz2 +https://conda.anaconda.org/conda-forge/linux-64/conda-package-handling-1.7.3-py36h8f6f2f9_0.tar.bz2 +https://conda.anaconda.org/conda-forge/linux-64/cryptography-35.0.0-py36hb60f036_0.tar.bz2 +https://repo.anaconda.com/pkgs/main/linux-64/grpcio-1.36.1-py36h2157cd5_1.conda +https://repo.anaconda.com/pkgs/main/linux-64/markdown-3.3.4-py36h06a4308_0.conda +https://conda.anaconda.org/conda-forge/noarch/munch-2.5.0-py_0.tar.bz2 +https://repo.anaconda.com/pkgs/main/linux-64/numpy-base-1.17.4-py36hde5b4d6_0.conda +https://repo.continuum.io/pkgs/main/linux-64/pip-21.2.2-py36h06a4308_0.tar.bz2 +https://repo.anaconda.com/pkgs/main/linux-64/protobuf-3.17.2-py36h295c915_0.conda +https://conda.anaconda.org/conda-forge/noarch/pyopenssl-22.0.0-pyhd8ed1ab_1.tar.bz2 +https://conda.anaconda.org/conda-forge/noarch/urllib3-1.26.15-pyhd8ed1ab_0.conda +https://conda.anaconda.org/conda-forge/noarch/requests-2.28.1-pyhd8ed1ab_0.tar.bz2 +https://conda.anaconda.org/conda-forge/linux-64/conda-4.10.3-py36h5fab9bb_2.tar.bz2 +https://conda.anaconda.org/conda-forge/linux-64/cudatoolkit-dev-9.2-py36_2.tar.bz2 +https://repo.anaconda.com/pkgs/main/linux-64/h5py-2.10.0-py36hd6299e0_1.conda +https://repo.anaconda.com/pkgs/main/linux-64/imageio-2.6.1-py36_0.conda +https://repo.anaconda.com/pkgs/main/linux-64/matplotlib-3.1.3-py36_0.conda +https://repo.anaconda.com/pkgs/main/linux-64/matplotlib-base-3.1.3-py36hef1b27d_0.conda +https://repo.anaconda.com/pkgs/main/linux-64/mkl_fft-1.3.0-py36h54f3939_0.conda +https://repo.anaconda.com/pkgs/main/linux-64/mkl_random-1.1.1-py36h0573a6f_0.conda +https://repo.anaconda.com/pkgs/main/linux-64/numpy-1.17.4-py36hc1035e2_0.conda +https://repo.anaconda.com/pkgs/main/noarch/keras-applications-1.0.8-py_1.conda +https://repo.anaconda.com/pkgs/main/linux-64/scipy-1.5.2-py36h0b6359f_0.conda +https://repo.anaconda.com/pkgs/main/linux-64/tensorboard-1.12.2-py36he6710b0_0.conda +https://repo.anaconda.com/pkgs/main/noarch/keras-preprocessing-1.1.2-pyhd3eb1b0_0.conda +https://repo.anaconda.com/pkgs/main/linux-64/tensorflow-base-1.12.0-gpu_py36had579c0_0.conda +https://repo.anaconda.com/pkgs/main/linux-64/tensorflow-1.12.0-gpu_py36he74679b_0.conda +https://repo.anaconda.com/pkgs/main/linux-64/tensorflow-gpu-1.12.0-h0d30ee6_0.conda diff --git a/SDFDiff_cameras.json b/SDFDiff_cameras.json new file mode 100644 index 0000000..9ab0fba --- /dev/null +++ b/SDFDiff_cameras.json @@ -0,0 +1,647 @@ +{ + "0": { + "forward": [ + 0.0, + 0.0, + 1.0 + ], + "fov_horizontal": 22.5, + "fov_vertical": 40, + "position": [ + 0.3382070064544678, + 0.38795384764671326, + -1.0419285595417023 + ], + "right": [ + 1.0, + 0.0, + 0.0 + ], + "up": [ + 0.0, + 1.0, + 0.0 + ] + }, + "1": { + "forward": [ + 1.0, + 0.0, + 2.220446049250313e-16 + ], + "fov_horizontal": 22.5, + "fov_vertical": 40, + "position": [ + -0.9646425247192383, + 0.38795384764671326, + 0.2609209716320035 + ], + "right": [ + 2.220446049250313e-16, + 0.0, + -1.0 + ], + "up": [ + 0.0, + 1.0, + 0.0 + ] + }, + "10": { + "forward": [ + 0.7071067811865476, + 0.0, + 0.7071067811865475 + ], + "fov_horizontal": 22.5, + "fov_vertical": 40, + "position": [ + -0.5830467319041741, + 0.38795384764671326, + -0.6603327667266379 + ], + "right": [ + 0.7071067811865475, + 0.0, + -0.7071067811865476 + ], + "up": [ + 0.0, + 1.0, + 0.0 + ] + }, + "11": { + "forward": [ + 0.7071067811865477, + 0.0, + -0.7071067811865475 + ], + "fov_horizontal": 22.5, + "fov_vertical": 40, + "position": [ + -0.5830467319041742, + 0.38795384764671326, + 1.1821747099906454 + ], + "right": [ + -0.7071067811865475, + 0.0, + -0.7071067811865477 + ], + "up": [ + 0.0, + 1.0, + 0.0 + ] + }, + "12": { + "forward": [ + -0.7071067811865476, + 0.0, + -0.7071067811865476 + ], + "fov_horizontal": 22.5, + "fov_vertical": 40, + "position": [ + 1.2594607448131094, + 0.38795384764671326, + 1.1821747099906454 + ], + "right": [ + -0.7071067811865476, + 0.0, + 0.7071067811865476 + ], + "up": [ + 0.0, + 1.0, + 0.0 + ] + }, + "13": { + "forward": [ + -0.7071067811865477, + 0.0, + 0.7071067811865475 + ], + "fov_horizontal": 22.5, + "fov_vertical": 40, + "position": [ + 1.2594607448131097, + 0.38795384764671326, + -0.6603327667266379 + ], + "right": [ + 0.7071067811865475, + 0.0, + 0.7071067811865477 + ], + "up": [ + 0.0, + 1.0, + 0.0 + ] + }, + "14": { + "forward": [ + 0.0, + 0.7071067811865476, + 0.7071067811865475 + ], + "fov_horizontal": 22.5, + "fov_vertical": 40, + "position": [ + 0.3382070064544678, + -0.5332998907119286, + -0.6603327667266379 + ], + "right": [ + 1.0, + 0.0, + 0.0 + ], + "up": [ + 0.0, + 0.7071067811865475, + -0.7071067811865476 + ] + }, + "15": { + "forward": [ + 0.7071067811865475, + 0.7071067811865476, + 1.570092458683775e-16 + ], + "fov_horizontal": 22.5, + "fov_vertical": 40, + "position": [ + -0.5830467319041739, + -0.5332998907119286, + 0.26092097163200356 + ], + "right": [ + 2.220446049250313e-16, + 0.0, + -1.0 + ], + "up": [ + -0.7071067811865476, + 0.7071067811865475, + -1.5700924586837752e-16 + ] + }, + "16": { + "forward": [ + 8.659560562354932e-17, + 0.7071067811865476, + -0.7071067811865475 + ], + "fov_horizontal": 22.5, + "fov_vertical": 40, + "position": [ + 0.33820700645446766, + -0.5332998907119286, + 1.1821747099906454 + ], + "right": [ + -1.0, + 0.0, + -1.2246467991473532e-16 + ], + "up": [ + -8.659560562354934e-17, + 0.7071067811865475, + 0.7071067811865476 + ] + }, + "17": { + "forward": [ + -0.7071067811865475, + 0.7071067811865476, + -1.570092458683775e-16 + ], + "fov_horizontal": 22.5, + "fov_vertical": 40, + "position": [ + 1.2594607448131094, + -0.5332998907119286, + 0.260920971632004 + ], + "right": [ + -2.220446049250313e-16, + 0.0, + 1.0 + ], + "up": [ + 0.7071067811865476, + 0.7071067811865475, + 1.5700924586837752e-16 + ] + }, + "18": { + "forward": [ + 0.4082482904638631, + -0.8164965809277261, + 0.408248290463863 + ], + "fov_horizontal": 22.5, + "fov_vertical": 40, + "position": [ + -0.1936790873788431, + 1.451726035313335, + -0.2709651222013071 + ], + "right": [ + 0.7071067811865475, + 0.0, + -0.7071067811865476 + ], + "up": [ + 0.5773502691896258, + 0.5773502691896257, + 0.5773502691896257 + ] + }, + "19": { + "forward": [ + 0.4082482904638631, + -0.816496580927726, + -0.40824829046386296 + ], + "fov_horizontal": 22.5, + "fov_vertical": 40, + "position": [ + -0.1936790873788432, + 1.451726035313335, + 0.7928070654653147 + ], + "right": [ + -0.7071067811865475, + 0.0, + -0.7071067811865477 + ], + "up": [ + 0.5773502691896258, + 0.5773502691896257, + -0.5773502691896257 + ] + }, + "2": { + "forward": [ + 1.2246467991473532e-16, + 0.0, + -1.0 + ], + "fov_horizontal": 22.5, + "fov_vertical": 40, + "position": [ + 0.3382070064544676, + 0.38795384764671326, + 1.5637705028057098 + ], + "right": [ + -1.0, + 0.0, + -1.2246467991473532e-16 + ], + "up": [ + 0.0, + 1.0, + 0.0 + ] + }, + "20": { + "forward": [ + -0.408248290463863, + -0.8164965809277261, + -0.408248290463863 + ], + "fov_horizontal": 22.5, + "fov_vertical": 40, + "position": [ + 0.8700931002877786, + 1.451726035313335, + 0.7928070654653147 + ], + "right": [ + -0.7071067811865476, + 0.0, + 0.7071067811865476 + ], + "up": [ + -0.5773502691896257, + 0.5773502691896257, + -0.5773502691896257 + ] + }, + "21": { + "forward": [ + -0.4082482904638631, + -0.816496580927726, + 0.40824829046386296 + ], + "fov_horizontal": 22.5, + "fov_vertical": 40, + "position": [ + 0.8700931002877788, + 1.451726035313335, + -0.2709651222013071 + ], + "right": [ + 0.7071067811865475, + 0.0, + 0.7071067811865477 + ], + "up": [ + -0.5773502691896258, + 0.5773502691896257, + 0.5773502691896257 + ] + }, + "22": { + "forward": [ + 0.4082482904638631, + 0.8164965809277261, + 0.408248290463863 + ], + "fov_horizontal": 22.5, + "fov_vertical": 40, + "position": [ + -0.1936790873788431, + -0.6758183400199085, + -0.2709651222013071 + ], + "right": [ + 0.7071067811865475, + 0.0, + -0.7071067811865476 + ], + "up": [ + -0.5773502691896258, + 0.5773502691896257, + -0.5773502691896257 + ] + }, + "23": { + "forward": [ + 0.4082482904638631, + 0.816496580927726, + -0.40824829046386296 + ], + "fov_horizontal": 22.5, + "fov_vertical": 40, + "position": [ + -0.1936790873788432, + -0.6758183400199085, + 0.7928070654653147 + ], + "right": [ + -0.7071067811865475, + 0.0, + -0.7071067811865477 + ], + "up": [ + -0.5773502691896258, + 0.5773502691896257, + 0.5773502691896257 + ] + }, + "24": { + "forward": [ + -0.408248290463863, + 0.8164965809277261, + -0.408248290463863 + ], + "fov_horizontal": 22.5, + "fov_vertical": 40, + "position": [ + 0.8700931002877786, + -0.6758183400199085, + 0.7928070654653147 + ], + "right": [ + -0.7071067811865476, + 0.0, + 0.7071067811865476 + ], + "up": [ + 0.5773502691896257, + 0.5773502691896257, + 0.5773502691896257 + ] + }, + "25": { + "forward": [ + -0.4082482904638631, + 0.816496580927726, + 0.40824829046386296 + ], + "fov_horizontal": 22.5, + "fov_vertical": 40, + "position": [ + 0.8700931002877788, + -0.6758183400199085, + -0.2709651222013071 + ], + "right": [ + 0.7071067811865475, + 0.0, + 0.7071067811865477 + ], + "up": [ + 0.5773502691896258, + 0.5773502691896257, + -0.5773502691896257 + ] + }, + "3": { + "forward": [ + -1.0, + 0.0, + -2.220446049250313e-16 + ], + "fov_horizontal": 22.5, + "fov_vertical": 40, + "position": [ + 1.6410565376281738, + 0.38795384764671326, + 0.26092097163200406 + ], + "right": [ + -2.220446049250313e-16, + 0.0, + 1.0 + ], + "up": [ + 0.0, + 1.0, + 0.0 + ] + }, + "4": { + "forward": [ + 0.0, + -1.0, + 2.220446049250313e-16 + ], + "fov_horizontal": 22.5, + "fov_vertical": 40, + "position": [ + 0.3382070064544678, + 1.6908033788204193, + 0.2609209716320035 + ], + "right": [ + 1.0, + 0.0, + 0.0 + ], + "up": [ + 0.0, + 2.220446049250313e-16, + 1.0 + ] + }, + "5": { + "forward": [ + 0.0, + 1.0, + 2.220446049250313e-16 + ], + "fov_horizontal": 22.5, + "fov_vertical": 40, + "position": [ + 0.3382070064544678, + -0.9148956835269928, + 0.2609209716320035 + ], + "right": [ + 1.0, + 0.0, + 0.0 + ], + "up": [ + 0.0, + 2.220446049250313e-16, + -1.0 + ] + }, + "6": { + "forward": [ + 0.0, + -0.7071067811865476, + 0.7071067811865475 + ], + "fov_horizontal": 22.5, + "fov_vertical": 40, + "position": [ + 0.3382070064544678, + 1.3092075860053551, + -0.6603327667266379 + ], + "right": [ + 1.0, + 0.0, + 0.0 + ], + "up": [ + 0.0, + 0.7071067811865475, + 0.7071067811865476 + ] + }, + "7": { + "forward": [ + 0.7071067811865475, + -0.7071067811865476, + 1.570092458683775e-16 + ], + "fov_horizontal": 22.5, + "fov_vertical": 40, + "position": [ + -0.5830467319041739, + 1.3092075860053551, + 0.26092097163200356 + ], + "right": [ + 2.220446049250313e-16, + 0.0, + -1.0 + ], + "up": [ + 0.7071067811865476, + 0.7071067811865475, + 1.5700924586837752e-16 + ] + }, + "8": { + "forward": [ + 8.659560562354932e-17, + -0.7071067811865476, + -0.7071067811865475 + ], + "fov_horizontal": 22.5, + "fov_vertical": 40, + "position": [ + 0.33820700645446766, + 1.3092075860053551, + 1.1821747099906454 + ], + "right": [ + -1.0, + 0.0, + -1.2246467991473532e-16 + ], + "up": [ + 8.659560562354934e-17, + 0.7071067811865475, + -0.7071067811865476 + ] + }, + "9": { + "forward": [ + -0.7071067811865475, + -0.7071067811865476, + -1.570092458683775e-16 + ], + "fov_horizontal": 22.5, + "fov_vertical": 40, + "position": [ + 1.2594607448131094, + 1.3092075860053551, + 0.260920971632004 + ], + "right": [ + -2.220446049250313e-16, + 0.0, + 1.0 + ], + "up": [ + -0.7071067811865476, + 0.7071067811865475, + -1.5700924586837752e-16 + ] + }, + "focus": [ + 0.3382070094283088, + 0.38795384153014023, + 0.2609209839653898 + ], + "focus_error": 0.0012484445282397506, + "fov_horizontal_average": 22.5, + "fov_vertical_average": 40.0, + "marker_width": 0.4909, + "scale_y": 1.77, + "volume_offset": [ + 0.08181666666666666, + -0.04462727272727273, + -0.004909 + ], + "volume_size": [ + 0.4909, + 0.868893, + 0.4909 + ], + "volume_width": 0.4909 +} \ No newline at end of file diff --git a/common_setups.py b/common_setups.py new file mode 100644 index 0000000..8a91d20 --- /dev/null +++ b/common_setups.py @@ -0,0 +1,1612 @@ +import copy, munch, numbers, collections.abc + +import lib.scalar_schedule as sSchedule + +#https://stackoverflow.com/questions/3232943/update-value-of-a-nested-dictionary-of-varying-depth +def _CSETUPS_update_dict_recursive(d, u, deepcopy=False, new_key='KEEP'): + if deepcopy: + d = copy.deepcopy(d) + for k, v in u.items(): + if not k in d: + if new_key.upper()=='ERROR': + raise KeyError("Update key {} does not exisit in base dictionary.".format(k)) + elif new_key.upper()=='DISCARD': + continue + elif new_key.upper()=='KEEP': + pass + else: + raise ValueError("Unknown policy for new key: {}".format(new_key)) + if isinstance(v, collections.abc.Mapping): + if k in d and not isinstance(d[k], collections.abc.Mapping): + # if something that is not a Mapping is updated with a Mapping + #e.g. a default constant schedule (int, float, list) with a complex one (dict {"type":, ...}) + if isinstance(d[k], (numbers.Number, list)) and "type" in v and isinstance(v["type"], str) and v["type"].upper() in ['SCHEDULE', 'CONST','LINEAR','EXPONENTIAL','ROOT_DECAY'] and "start" in v: + d[k] = sSchedule._get_base_schedule() + else: + d[k] = {} + d[k] = _CSETUPS_update_dict_recursive(d.get(k, {}), v, deepcopy=deepcopy, new_key=new_key) + else: + if deepcopy: + d[k] = copy.deepcopy(v) + else: + d[k] = v + return d + + +####################### +# --- Setup Modules --- +####################### + +#crop and main-opt growth with new loss balaning (after scale fixes) +RECONSTRUCT_SEQUENCE_SETUP_BASE = { + 'title':'seq_test', + 'desc':'sequence reconstruction run description', + 'paths':{ + 'base':"./runs", + 'group':"sequence_recon_test", + }, + 'rendering':{ + 'monochrome':False, + 'luma':[0.2126,0.7152,0.0722], #[0.299,0.587,0.144] #https://en.wikipedia.org/wiki/Luma_(video) + 'filter_mode':'LINEAR', #NEAREST, LINEAR + 'mip':{ + 'mode':'LINEAR', #NONE, NEAREST, LINEAR + 'level':4, + 'bias':0.0, + }, + 'blend_mode':'BEER_LAMBERT', #BEER_LAMBERT, ALPHA, ADDITIVE + 'sample_gradients':True, + 'boundary': None, + 'SDF_threshold': 0.02, + + 'steps_per_cycle':24, + 'num_images': 24, + + 'main_camera':{ + 'base_resolution':[256,1920,1080], #z(depth), y(height), x(width) + 'resolution_scale':1./3., # only for xy + 'fov':40, + 'near':0.3, + 'distance':0.8, + 'far':1.3, + }, + 'target_cameras':{ + 'calibration_file':"scalaFlow_cameras.json", + 'camera_ids':[2,1,0,4,3], + 'crop_frustum':False, # crop frustum grid to AABB of vidual hull. for performance + 'crop_frustum_pad':2, # view space cells + }, + + 'allow_static_cameras':False, + 'allow_fused_rendering':True, + + 'background':{ + 'type':'COLOR', #'CAM', 'COLOR', 'NONE'; only for vis-rendering, not used in optimization + 'color': [0,0.5,1.0], #[0,0.5,0], + }, + + 'lighting':{ + #'scattering_ratio':1., + 'ambient_intensity':0.64, + 'initial_intensity':0.85, + 'shadow_resolution':[256,196,196], #DHW + }, + "velocity_scale":1024, + "synthetic_target":{ + 'filter_mode':'LINEAR', + 'blend_mode':'BEER_LAMBERT', + 'ambient_intensity':0.64, + 'initial_intensity':0.85, + } + },#rendering + 'data':{ + "rand_seed_global": 460585320, + 'run_dirs':['./runs/test-and-debug','./runs/sequence_recon_test'], + 'grid_size':128, #x and z resolution/grid size, y is ceil(this*scale_y) from callibration (scale_y ~ 1.77) + 'y_scale': 2, #"SF" + 'clip_grid':False, + 'clip_grid_pad':6, + 'crop_grid':True, + 'hull':'TARGETS', #ALL, TARGETS, ROT, [, ], empty==ALL + + 'simulation':0, + 'start':140, + 'stop':142, #exclusive + 'step':1, + 'scalarFlow_frame_offset':-11, + "SDF": False, + 'MS_volume':False, + 'MS_images':False, + 'density':{ + 'scale': 1, + #'initial_value':'HULL_TIGHT',#"CONST", "ESTIMATE", "HULL" or path to an npz file '[RUNID:000000-000000]frame_{frame:06d}/density_pre-opt.npz' + 'initial_value':'data/scalarFlow/sim_{sim:06d}/reconstruction/density_{frame:06d}.npz', + 'density_type': "SF", #OWN SF MANTA + 'min':0.0, + 'max':256.0, + 'target_type': "RAW", #PREPROC, SYNTHETIC + 'render_targets': False, + 'target': 'data/scalarFlow/sim_{sim:06d}/input/cam/imgsUnproc_{frame:06d}.npz', # 'data/scalarFlow/sim_{:06d}/input/cam/imgsUnproc_{:06d}.npz', + 'target_preproc': 'data/scalarFlow/sim_{sim:06d}/input/postprocessed/imgs_{frame:06d}.npz', + 'target_flip_y': False, + 'target_cam_ids':"ALL",#[0,1,2,3,4], #which ScalarFlow camera targets to use. 0:center, 1:center-left, 2:left, 3:right, 4:center-right (CW starting from center) + 'target_threshold':4e-2, #only used for disc dataset + 'target_scale': 1.5, + 'hull_image_blur_std':1.0, + 'hull_volume_blur_std':0.5, + 'hull_smooth_blur_std':0.0, + 'hull_threshold':4e-2, + 'inflow':{ + 'active':True, + 'hull_height':10, + 'height':'MAX', + }, + 'scalarFlow_reconstruction':'data/scalarFlow/sim_{sim:06d}/reconstruction/density_{frame:06d}.npz', + 'synthetic_target_density_scale':1., + }, + 'velocity':{ + 'initial_value':'data/scalarFlow/sim_{sim:06d}/reconstruction/velocity_{frame:06d}.npz', #'RAND', + 'load_step':1, + 'init_std':0.1, + 'init_mask':'HULL_TIGHT_NEXT', #NONE HULL, HULL_TIGHT + 'boundary':'CLAMP', #BORDER (closed 0 bounds), CLAMP (open bounds) + 'scalarFlow_reconstruction':'data/scalarFlow/sim_{sim:06d}/reconstruction/velocity_{frame:06d}.npz', + }, + 'initial_buoyancy':[0.,0.,0.], + 'discriminator':{ + 'simulations':[0,6], + 'frames':[45,145, 1], + 'target_type': "RAW", #PREPROC, (SYNTHETIC not supported) + 'initial_value':'data/scalarFlow/sim_{sim:06d}/reconstruction/density_{frame:06d}.npz', #path to density. needed for target rendering + 'density_type': "SF", #OWN SF MANTA + 'render_targets': False, + 'density_type': "SF", #OWN SF MANTA + 'target': 'data/scalarFlow/sim_{sim:06d}/input/cam/imgsUnproc_{frame:06d}.npz', # 'data/scalarFlow/sim_{:06d}/input/cam/imgsUnproc_{:06d}.npz', + 'target_preproc': 'data/scalarFlow/sim_{sim:06d}/input/postprocessed/imgs_{frame:06d}.npz', + #augmentation + 'crop_size':[96,96], #HW, input size to disc + 'scale_input_to_crop':False, #resize all discriminator input to its base input resolution (crop_size) before augmentation + 'real_res_down': 4, + 'scale_real_to_cam':True, #scale real data resolution to current discriminator fake-sample camera (to match resolution growth) + 'scale_range':[0.85,1.15], + 'rotation_mode': "90", + # 'resolution_fake':[256,1920/4,1080/4], + # 'resolution_scales_fake':[1, 1/2], + # 'resolution_loss':[256,1920/8,1080/8], + 'scale_real':[0.8, 1.8], #range for random intensity scale on real samples + 'scale_fake':[0.7, 1.4], + # 'scale_loss':[0.8, 1.2], + 'gamma_real':[0.5,2], #range for random gamma correction on real samples (value here is inverse gamma) + 'gamma_fake':[0.5,2], #range for random gamma correction on fake samples (value here is inverse gamma) + # 'gamma_loss':[0.5,2], #range for random gamma correction applied to input when evaluating the disc as loss (for density) (value here is inverse gamma) + + }, + 'load_sequence':None, #only for rendering without optimization + 'load_sequence_pre_opt':False, + 'synth_shapes':{ #dataset of randomly generated shapes + 'active':True, #overrides other data loader + 'max_translation': 0.4, # + 'shape_types': 0, # single type-id or list of type-ids. 0=smooth sphere, 1=cube, 2=cube edges, 3=cube vertices, 4=sphere + 'init_center': True, + }, + 'resource_device': '/cpu:0',#'/cpu:0' + },#data + 'training':{ + 'iterations':5000, + 'start_iteration': 0, + 'frame_order':'FWD', #FWD, BWD, RAND + 'sequence_length': sSchedule.setup_constant_schedule(start=-1), #shedule for sequence length. cast to int, -1 to use full length + + + 'resource_device':'/cpu:0', #'/cpu:0', '/gpu:0' + + #'loss':'L2', + 'train_res_down':6, + 'loss_active_eps':1e-18, #minimum absolute value of a loss scale for the loss to be considered active (to prevent losses scaled to (almost) 0 from being evaluated) + + 'randomization':{ + 'transform':False, + 'sequence_length':False, + 'grid_size_relative': 1.0, # minimum relative grid size, 1 to diable grid size randomizaton + 'grid_size_min': 3, # minimum absolute grid size + + 'grow_mode': None, # None, "RAND", "ITERATE", "RANDINTERVAL" + + # range of random intensity scale for images (inputs and targets) + 'scale_images_min':1, + 'scale_images_max':1, + # range of random density scale for velocity training + 'scale_density_min':1, + 'scale_density_max':1, + + 'inputs':True, + 'input_weights':None, #relative weights for targets, none for uniform + 'min_inputs':1, + 'max_inputs':5, + 'num_inputs_weights':None, #relative weights for number of targets, none for uniform + + 'targets':False, + 'target_weights':None, #relative weights for targets, none for uniform + 'min_targets':1, + 'max_targets':5, + 'num_targets_weights':None, #relative weights for number of targets, none for uniform + }, + 'density':{ + 'optim_beta':0.9, + 'use_hull':True, + 'warp_clamp':"MC_SMOOTH", + 'camera_jitter':False, + 'scale_render_grads_sharpness':0.0, + 'error_functions':{ + 'raw_target_loss':'SE', + 'preprocessed_target_loss':'SE', + 'volume_target_loss':'SE', + 'volume_proxy_loss':'SE', + 'target_depth_smoothness_loss':'SE', + 'hull':'SE', + 'negative':'SE', + 'smoothness_loss':'SE', + 'smoothness_loss_2':'SE', + 'temporal_smoothness_loss':'SE', + 'warp_loss':'SE', + 'center_loss':'SE', + 'SDF_pos_loss':'AE', + }, + 'pre_optimization':True, #whether pre-optim will run for density, affects forward propagation/advection of state and optimization + # to only have fwd advection without optimization set iterations to 0 + 'pre_opt':{ + 'first':{ #settings for first frame + 'iterations':0,#30000, + 'learning_rate':sSchedule.setup_exponential_schedule(start=3.0, base=0.5, scale=2/30000), #{'type':'exponential', 'start':3.0, 'base':0.5, 'scale':2/30000}, + + 'raw_target_loss':8.7e-7 *20, + 'preprocessed_target_loss':0., + 'volume_target_loss':0., + 'target_depth_smoothness_loss':0., + 'hull':0., + 'negative':0., + 'smoothness_loss':0.0,#6.8e-14, + 'smoothness_neighbours':3, + 'smoothness_loss_2':0.0,#8.2e-14, + 'temporal_smoothness_loss':0.0,#0.2 + 'discriminator_loss':0.0, + 'warp_loss':0.0, + 'center_loss':0.0, + 'SDF_pos_loss':0.0, + 'regularization':0.0001, + }, + #settings for remaining frames + 'iterations':2400,#5000, + 'seq_init':"WARP", #WARP, COPY, BASE + 'learning_rate':sSchedule.setup_exponential_schedule(start=3.0, base=0.5, scale=1/3000), #{'type':'exponential', 'start':0.00005, 'base':0.5, 'scale':2/30000}, + + 'raw_target_loss':8.7e-7 *20, + 'preprocessed_target_loss':0., + 'volume_target_loss':0., + 'target_depth_smoothness_loss':0., + 'hull':0., + 'negative':0., + 'smoothness_loss':0.0,#6.8e-14, + 'smoothness_neighbours':3, + 'smoothness_loss_2':0.0,#8.2e-14, + 'temporal_smoothness_loss':0.0,#0.2 + 'discriminator_loss':0.0, + 'warp_loss':0.0, + 'center_loss':0.0, + 'SDF_pos_loss':0.0, + 'regularization':0.0001, + + 'inspect_gradients':1, + 'grow':{ + "factor":1.2,#2. factor per interval, max down-scaling is factor^len(intervals) + 'intervals':[], + }, + }, + + 'grow_lifting_skip': None, + 'grow_lifting_train': None, + 'grow_lifting_lr': None, + + 'grow_lifting_residual': None, + 'grow_volenc_residual': None, + + 'learning_rate':sSchedule.setup_exponential_schedule(start=2.45, base=0.5, scale=2/30000),#0.00015, #[0.00001,0.00001,0.0001, 0.00009/20000, 4000], + + 'raw_target_loss':8.7e-7 *20,#for AE; for SE *40; for Huber *80 + 'preprocessed_target_loss':0., + 'volume_target_loss':0., + 'volume_proxy_loss':0., + 'target_depth_smoothness_loss':0., + 'hull':0.,#1e-12, + 'negative':0.,#1e-12, + + 'smoothness_loss':0.0, + 'smoothness_neighbours':3, # the kind of neighbourhood to consider in the edge filter (e.g. wether to use diagonals), NOT the kernel size. + 'smoothness_loss_2':0.0, + 'temporal_smoothness_loss':0.0,#0.2 + + 'discriminator_loss':1.5e-5, + 'warp_loss':[6.7e-11 *4,6.7e-11 *4,13.4e-11 *4, 6.7e-11 *4/2000, 2000],#for AE; for SE *8, for Huber *24 + 'center_loss':0.0, + 'SDF_pos_loss':0.0,#for AE; for SE *8, for Huber *24 + 'regularization':0.0001, + + 'main_warp_fwd':False, + 'warp_gradients':{ + 'weight':sSchedule.setup_constant_schedule(start=1.0), + 'active':False, + 'decay':sSchedule.setup_constant_schedule(start=0.9), #[0,1], lower is faster decay + 'update_first_only':False, + }, + "view_interpolation":{ + "steps":0, + }, + + 'grow':{ + "factor":1.2,#2. factor per interval, max down-scaling is factor^len(intervals) + "pre_grow_actions":[],# "WARP", list, unused + "post_grow_actions":[],# "WARP", list + #iterations for each grow step, empty to disable + 'intervals':[], + }, + }, + 'velocity':{ + 'optim_beta':0.9, + 'warp_order':2, + 'warp_clamp':"MC_SMOOTH", + 'error_functions':{ + 'volume_target_loss':'SE', + 'density_warp_loss':'SE', + 'density_proxy_warp_loss':'SE', + 'density_target_warp_loss':'SE', + 'velocity_warp_loss':'SE', + 'divergence_loss':'SE', + 'magnitude_loss':'SE', + 'CFL_loss':'SE', + 'MS_coherence_loss':'SE', + }, + 'pre_optimization':True, #whether pre-optim will run for velocity, affects forward propagation/advection of state and optimization + 'pre_opt':{ + 'first':{ #settings for first frame + 'iterations':0, + 'learning_rate':0.04, + + 'volume_target_loss':0., + 'density_warp_loss':8.2e-11 *5, + 'density_proxy_warp_loss':0, + 'density_target_warp_loss':8.2e-11 *5, + 'velocity_warp_loss':0.0, + 'smoothness_loss':0.0, + 'smoothness_neighbours':3, + 'cossim_loss':0.0, + 'divergence_loss':sSchedule.setup_exponential_schedule(start=4.3e-10 *2, base=1.2, scale=1/10000), #sSchedule.setup_linear_schedule_2(start=0, end=4.3e-10, steps=7000),# + #'divergence_normalize':0.0, + #adjust according to data.step, large values can lead to NaN. + 'magnitude_loss':0.0,#{'type':'exponential', 'start':4e-11, 'base':0.10, 'scale':1/5000}, + 'CFL_loss':0.0, + 'MS_coherence_loss':0.0, + 'regularization':0.0001, + 'grow':{ + "factor":1.2, + "scale_magnitude":True, + 'intervals':[200, 260, 330, 400, 460, 530, 600, 660, 800, 930, 1060, 1260], #7490 + }, + + }, + #settings for remaining frames + 'iterations':1200, + 'seq_init':"WARP", #WARP, COPY, BASE + 'learning_rate':0.02, + + 'volume_target_loss':0., + 'density_warp_loss':8.2e-11 *5, + 'density_proxy_warp_loss':0, + 'density_target_warp_loss':8.2e-11 *5, + 'velocity_warp_loss':0.0, + 'smoothness_loss':0.0, + 'smoothness_neighbours':3, + 'cossim_loss':0.0, + 'divergence_loss':4.3e-10 *6, + #'divergence_normalize':0.0, + 'magnitude_loss':0.0,#4e-12, + 'CFL_loss':0.0, + 'MS_coherence_loss':0.0, + 'regularization':0.0001, + + 'grow':{ + "factor":1.2,#2. factor per interval, max down-scaling is factor^len(intervals) + "scale_magnitude":True, + 'intervals':[], + }, + }, + + 'noise_std':sSchedule.setup_constant_schedule(start=0.0), + 'learning_rate':sSchedule.setup_exponential_schedule(start=0.02, base=0.5, scale=2/30000),#{'type':'exponential', 'max':0.02, 'start':0.02, 'base':0.5, 'scale':2/30000, 'offset':0}, + + # 'lr_decay':0.00, + + # 'loss':'L2', + 'volume_target_loss':0., + 'density_warp_loss':8.2e-11 *5,#for AE; for SE *10; for Huber *25 #influence of loss(A(dt, vt), dt+1) on velocity, can be a schedule + 'density_warp_loss_MS_weighting':sSchedule.setup_constant_schedule(start=1.0), + 'density_proxy_warp_loss':0, + 'density_target_warp_loss':8.2e-11 *5,#for AE; for SE *10; for Huber *25 #influence of loss(A(dt, vt), dt+1) on velocity, can be a schedule + 'density_target_warp_loss_MS_weighting':sSchedule.setup_constant_schedule(start=1.0), + 'velocity_warp_loss':sSchedule.setup_linear_schedule_2(start=1.35e-11 *3, end=1.35e-11 *6, steps=5000), #2.7e-12 *5,#for AE; for SE *10; for Huber *20 #influence of loss(A(vt, vt), vt+1) on velocity, can be a schedule + # 'velocity_warp_loss_MS_weighting':sSchedule.setup_constant_schedule(start=1.0), + + 'smoothness_loss':0.0, + 'smoothness_neighbours':3, # the kind of neighbourhood to consider in the edge filter (e.g. wether to use diagonals), NOT the kernel size. + 'cossim_loss':0.0, + + 'divergence_loss':sSchedule.setup_exponential_schedule(start=4.3e-10 *6, base=1.2, scale=1/500), + 'divergence_loss_MS_weighting':sSchedule.setup_constant_schedule(start=1.0), + 'divergence_normalize':0.0, + + 'magnitude_loss':0.0,#1e-12, + 'CFL_loss':0.0, + 'CFL_loss_MS_weighting':sSchedule.setup_constant_schedule(start=1.0), + 'MS_coherence_loss':0.0, + 'MS_coherence_loss_MS_weighting':sSchedule.setup_constant_schedule(start=1.0), + 'regularization':0.0001, + + 'warp_gradients':{ + 'weight':sSchedule.setup_constant_schedule(start=1.0), #affects warp gradients for velocity from backward dens warp, even if vel- warp gradients are inactive + 'active':False, + 'decay':sSchedule.setup_constant_schedule(start=0.9), #[0,1], lower is faster decay + }, + + 'grow':{ + "factor":1.2,#2. factor per interval, max down-scaling is factor^len(intervals) + "scale_magnitude":True, + 'intervals':[], + }, + }, + "MS_weighting": sSchedule.setup_constant_schedule(start=1.0), #level-relative loss weighting for multi-scale losses. here the "iteration" is the level index, which starts with 0 at the COARSEST resolution. + "allow_MS_losses": True, + + 'optimize_buoyancy':False, + "light":{ + "optimize":False, + 'optim_beta':0.9, + "min":0.01, + "max":6.0, + "learning_rate":{'type':'exponential', 'start':0.001, 'base':1, 'scale':0}, + }, + # "scattering":{ + # "optimize":False, + # 'optim_beta':0.9, + # "min":0.01, + # "max":1.0, + # "learning_rate":{'type':'exponential', 'start':0.001, 'base':1, 'scale':0}, + # }, + + + 'discriminator':{ + 'active':False, + 'model':None,#'[RUNID:200227-162722]disc_model.h5',# + 'loss_type':"RaLSGAN", #"SGAN", "RpSGAN", "RpLSGAN", "RaSGAN", "RaLSGAN" + 'target_label':1.0,#0.9, #0.9 for label smoothing, 1.0 for LS-GAN + # l4s + 'layers':[16,16,24,24,32,32,32,64,64,64,16, 4], + 'stride':[ 2, 1, 2, 1, 2, 1, 1, 2, 1, 1, 1, 1], + 'kernel_size':4, + 'padding':'MIRROR', #NONE(=valid), ZERO(=same), MIRROR(=tf.pad(REFLECT)) + 'activation':'lrelu', # relu or lrelu + 'activation_alpha':0.2, # leak for lrelu + 'use_fc': None, # list of filters, usually makes disc stronger + 'noise_std':0.0, + 'start_delay':0, #50, + 'pre_opt':{ + 'first':{ + 'train':False, + 'learning_rate':{'type':'exponential', 'start':4e-4, 'base':0.5, 'scale':4/30000}, + # 'steps':1, + 'regularization':0.002, + }, + 'train':False, + 'learning_rate':1.6e-4, + # 'steps':1, + 'regularization':0.002, + }, + 'train':True, + 'learning_rate':2e-4, + 'steps':1, + 'regularization':0.002, + 'optim_beta':0.5, + + 'grow':{ # not yet used + "factor":2.,#2. factor per interval, max down-scaling is factor^len(intervals) + #iterations for each grow step, empty to disable + 'intervals':[] + }, + + 'conditional_hull':False, + 'temporal_input':{ + 'active':False, + 'step_range':(-3,4,1), #-3 to 3 inclusive, 0 will be removed + }, + 'num_real':4, + 'cam_res_down':6, + 'num_fake':3, + 'fake_camera_jitter':False, + "history":{ + 'load':None, + 'samples':4, #use older samples as fake samples as well. 0 to disable + 'size':800, # + 'keep_chance':0.01, # chance to put a rendered sample in the history buffer + 'save':False, + 'sequence_reuse':True, + 'reset_on_density_grow':True, + # 'reset_on_discriminator_grow':False, + }, + + # 'sequence_reuse':True, + },#discriminator + 'summary_interval':100, + 'checkpoint_interval':500, + },#training + 'validation':{ + 'output_interval':100, + 'stats':True, + 'cmp_vol_targets':False, + 'cmp_scalarFlow':False, + 'cmp_scalarFlow_render':False, + 'warp_test':["BASE", "NO_INFLOW", "ONLY_INFLOW"], + 'warp_test_render':False, + 'render_cycle':False, + 'render_cycle_steps':8, + 'render_density':True, + 'render_shadow':True, + 'render_target':True, + 'render_velocity':True, + 'render_MS':False, + # if data.synth_shapes.active + 'synth_data_seed': 1802168824, + 'synth_data_shape_types':0, #for training mode + 'synth_data_eval_setup':"SPHERE", #pre-made test-cases for evaluation mode # SF, SPHERE, CUBE, ROTCUBE + }, + 'debug':{ + 'print_weight_grad_stats':False, + 'target_dump_samples':False, + 'disc_dump_samples':False, + }, +} + +# RECONSTRUCT_SEQUENCE_SETUP_GROW = { + # "training":{ + # 'iterations':7000, + # "density":{ + # 'pre_opt':{ + # 'first':{ + # 'iterations':400, + # }, + # 'iterations':400, + # }, + # 'grow':{ + # "factor":1.2, + # "pre_grow_actions":[], + # "post_grow_actions":[], + # 'intervals':[200,300,400,500,500,600,700,800], #4000 + # #'intervals':[400,400,400,400,400,400,400,400], #3200 + # }, + # }, + # 'velocity':{ + # 'pre_opt':{ + # 'first':{ + # 'iterations':5000, + # 'divergence_loss':sSchedule.setup_exponential_schedule(start=4.3e-10 *2, base=1.2, scale=1/1000), + # 'grow':{ + # "factor":1.2, + # "scale_magnitude":True, + # 'intervals':[660, 860, 1100, 1300], #3920 + # }, + # }, + # 'iterations':400, + # 'divergence_loss':4.3e-10 *6, + # }, + # 'divergence_loss':sSchedule.setup_exponential_schedule(start=4.3e-10 *6, base=1.2, scale=1/700), + # 'grow':{ + # "factor":1.2, + # "scale_magnitude":True, + # 'intervals':[200,300,400,500,500,600,700,800], #4000 + # }, + # }, + # }, +# } +# RECONSTRUCT_SEQUENCE_SETUP_GROW = _CSETUPS_update_dict_recursive(RECONSTRUCT_SEQUENCE_SETUP_BASE, RECONSTRUCT_SEQUENCE_SETUP_GROW, deepcopy=True, new_key='ERROR') + +# RECONSTRUCT_SEQUENCE_SETUP_GROW_DGRADWARP = { + # "training":{ + # 'frame_order':'BWD', + # "density":{ + # 'main_warp_fwd':True, + # 'warp_gradients':{ + # 'weight':1.0, + # 'active':True, + # 'decay':0.9, + # 'update_first_only':True, + # }, + # }, + # 'velocity':{ + # 'warp_gradients':{ + # 'weight':1.0, + # 'active':False, + # 'decay':0.9, + # }, + # }, + # }, +# } + +# RECONSTRUCT_SEQUENCE_SETUP_GROW_DGRADWARP = _CSETUPS_update_dict_recursive(RECONSTRUCT_SEQUENCE_SETUP_GROW, RECONSTRUCT_SEQUENCE_SETUP_GROW_DGRADWARP, deepcopy=True, new_key='ERROR') + +# RECONSTRUCT_SEQUENCE_SETUP_GROW_DGRADWARP_FRONT = { + # "desc":"front-loaded training. 50% longer pre-training, linear growth intervals (longer low res, shorter high res)", + # "training":{ + # 'iterations':4200, + # "density":{ + # 'pre_opt':{ + # 'first':{ + # 'iterations':600, + # }, + # 'iterations':600, + # }, + # 'grow':{ + # "factor":1.2, + # "pre_grow_actions":[], + # "post_grow_actions":[], + # #'intervals':[200,300,400,500,500,600,700,800], #4000 + # 'intervals':[400,400,400,400,400,400,400,400], #3200 + # }, + # }, + # 'velocity':{ + # 'pre_opt':{ + # 'first':{ + # 'iterations':6000, + # 'divergence_loss':sSchedule.setup_exponential_schedule(start=4.3e-10 *2, base=1.2, scale=1/1000), + # 'grow':{ + # "factor":1.2, + # "scale_magnitude":True, + # 'intervals':[1000, 1000, 1000, 1000], #4000 + # }, + # }, + # 'iterations':600, + # 'divergence_loss':4.3e-10 *6, + # }, + # 'divergence_loss':sSchedule.setup_exponential_schedule(start=4.3e-10 *6, base=1.2, scale=1/400), + # 'grow':{ + # "factor":1.2, + # "scale_magnitude":True, + # 'intervals':[400,400,400,400,400,400,400,400], #3200 + # }, + # }, + # }, +# } + +# RECONSTRUCT_SEQUENCE_SETUP_GROW_DGRADWARP_FRONT = _CSETUPS_update_dict_recursive(RECONSTRUCT_SEQUENCE_SETUP_GROW_DGRADWARP, RECONSTRUCT_SEQUENCE_SETUP_GROW_DGRADWARP_FRONT, deepcopy=True, new_key='ERROR') + +""" +def __init__(self, dimension=2, num_levels=3, level_scale_factor=2, input_channels=3, \ + input_levels=2, create_inputs=True, input_blocks=None, share_input_layer=True, \ + input_conv_filters=1, input_conv_kernel_size=1, \ + down_mode="STRIDED", down_conv_filters=None, down_conv_kernel_size=4, share_down_layer=True, \ + encoder_filters=[[1]], encoder_kernel_sizes=[[3]], \ + encoder_resblocks=None, share_encoder=False, \ + decoder_filters=[[1]], decoder_kernel_sizes=[[3]], \ + decoder_resblocks=None, share_decoder=False, \ + up_mode="NNSAMPLE", up_conv_filters=1, up_conv_kernel_size=4, share_up_layer=True, \ + skip_merge_mode="CONCAT", \ + output_levels=1, output_blocks=None, share_output_layer=True, output_activation="none", \ + output_channels=1, output_conv_kernel_size=1, output_mode="SINGLE", \ + conv_activation="relu", alpha=0.2, conv_padding="ZERO", \ + name="GrowingUNet", normalization="NONE", **kwargs) +""" +GROWINGUNET_CONFIG = { + #"dimension": 2, + #"num_levels": 3, + "level_scale_factor": 2, + + #"input_channels": 3, + "input_levels": 1, + "create_inputs": True, + "input_blocks": ["C:1-1"], + #"input_conv_filters": 1, + #"input_conv_kernel_size": 1, + "share_input_layer": False, + + "down_mode": "STRIDED", # STRIDED, NONE + "down_conv_filters": None, + "down_conv_kernel_size": 4, + "share_down_layer": True, + + "encoder_resblocks": ["RB:1-1_1-1"], + "share_encoder": False, + + "decoder_resblocks": ["RB:1-1_1-1"], + "share_decoder": False, + + "up_mode": "NNSAMPLE_CONV", + "up_conv_filters": 1, + "up_conv_kernel_size": 4, + "share_up_layer": True, + + "skip_merge_mode": "CONCAT", # CONCAT, WSUM, SUM + + #"output_levels": 1, + "output_blocks": None, + #"output_channels": 1, + "share_output_layer": True, + "output_activation": "none", # none, relu, lrelu + "output_conv_kernel_size": 1, + "output_mode": "SINGLE", # SINGLE, RESIDUAL + # global settings + "conv_activation": "relu", # none, relu, lrelu + "alpha": 0.2, # leak for lrelu + "conv_padding": "ZERO", # ZERO, MIRRIR + "normalization": "LAYER", # NONE, LAYER +} + +RECONSTRUCT_SEQUENCE_SETUP_NEURAL_DENSITY = { + "desc":"train neural networks to generate density directly from the targets.", + "data":{ + 'grid_size':48, #128, #resolution + 'y_scale': 2, # "SF" ~1.77 (16/9) + 'clip_grid':False, + 'clip_grid_pad':4, + 'crop_grid':True, + 'res_down_factor':128*5, #target data down scale factor is 6 (i.e. 1/6 of raw image resolution) at 128 base grid resolution + + 'start':20,#56,#40, + 'stop':141,#68,#52, #exclusive + 'step':2,#2, + # + "randomize":64, + "batch_size":4, + "sims":list(range(20)), + "sequence_step":1, + "sequence_length":1, + + "density":{ + 'target_type': "PREPROC", #RAW, PREPROC, SYNTHETIC + 'scale':1, + 'inflow':{ + 'active':False, + 'hull_height':4, + 'height':'MAX', + }, + }, + }, + "training":{ + 'iterations':0, + "view_encoder":{ + #"active":True, active= density.decoder.active or velocity.decoder.active + 'encoder': ['L'], #NETWORK, NONE + # 'layers':[4,8,16,16,16,4], + # 'stride':1, + # 'skip_connections':None, + # 'skip_mode':"CONCAT", + # 'kernel_size':4, + # 'padding':'MIRROR', #NONE(=valid), ZERO(=same), MIRROR(=tf.pad(REFLECT)) + # 'activation':'lrelu', # relu or lrelu + # 'activation_alpha':0.2, # leak for lrelu + # "load_encoder":None, + "lifting":"UNPROJECT", #UNPROJECT ;2D->3D method + #"load_lifting":None, #lifting could be NN + "merge":"MEAN", # SUM, MEAN ;method to merge 3D embeddings of multiple views + #"load_merge":None, #merging could be NN + + "model":{ + "num_levels": "VARIABLE", + "level_scale_factor": 2, + + "input_levels": 1, + "create_inputs": True, + "input_blocks": ["C:1-1"], + #"input_conv_filters": 1, + #"input_conv_kernel_size": 1, + "share_input_layer": False, + + "down_mode": "STRIDED", # STRIDED, NONE + "down_conv_filters": None, + "down_conv_kernel_size": 4, + "share_down_layer": True, + + "encoder_resblocks": ["RB:1-1_1-1"], + "share_encoder": False, + + "decoder_resblocks": ["RB:1-1_1-1"], + "share_decoder": False, + + "up_mode": "NNSAMPLE_CONV", + "up_conv_filters": 1, + "up_conv_kernel_size": 4, + "share_up_layer": True, + + "skip_merge_mode": "CONCAT", # CONCAT, WSUM, SUM + + #"output_levels": 1, + "output_blocks": None, + "output_channels": 1, + "share_output_layer": True, + "output_activation": "none", # none, relu, lrelu + "output_conv_kernel_size": 1, + "output_mode": "SINGLE", # SINGLE, RESIDUAL + # global settings + "conv_activation": "relu", # none, relu, lrelu + "alpha": 0.2, # leak for lrelu + "conv_padding": "ZERO", # ZERO, MIRRIR + "normalization": "LAYER", # NONE, LAYER + }, # or path to model file + "start_level": 0, + "min_grid_res": 8, + "train_mode": "ALL", #"ALL", "TOP", "TOP_DEC" + "train_mode_schedule": sSchedule.setup_boolean_schedule(start=True, offset=0), + "skip_merge_weight_schedule": sSchedule.setup_linear_schedule_2(start=1., end=1.0, steps=650, offset=250), + "grow_intervals": [], + }, + "volume_encoder":{ + "active":False, + #"model":"decoder_model", + "model":{ + "num_levels": "VARIABLE", + "level_scale_factor": 2, + + "input_levels": 1, + "create_inputs": True, + "input_blocks": ["C:1-1"], + #"input_conv_filters": 1, + #"input_conv_kernel_size": 1, + "share_input_layer": False, + + "down_mode": "STRIDED", # STRIDED, NONE + "down_conv_filters": None, + "down_conv_kernel_size": 4, + "share_down_layer": True, + + "encoder_resblocks": ["RB:1-1_1-1"], + "share_encoder": False, + + "decoder_resblocks": ["RB:1-1_1-1"], + "share_decoder": False, + + "up_mode": "NNSAMPLE_CONV", + "up_conv_filters": 1, + "up_conv_kernel_size": 4, + "share_up_layer": True, + + "skip_merge_mode": "CONCAT", # CONCAT, WSUM, SUM + + #"output_levels": 1, + "output_blocks": None, + "output_channels": 1, + "share_output_layer": True, + "output_activation": "none", # none, relu, lrelu + "output_conv_kernel_size": 1, + "output_mode": "SINGLE", # SINGLE, RESIDUAL + # global settings + "conv_activation": "relu", # none, relu, lrelu + "alpha": 0.2, # leak for lrelu + "conv_padding": "ZERO", # ZERO, MIRRIR + "normalization": "LAYER", # NONE, LAYER + }, # or path to model file + "start_level": 0, + "min_grid_res": 8, + "train_mode": "ALL", #"ALL", "TOP", "TOP_DEC" + "train_mode_schedule": sSchedule.setup_boolean_schedule(start=True, offset=0), + "skip_merge_weight_schedule": sSchedule.setup_linear_schedule_2(start=1., end=1.0, steps=650, offset=250), + "grow_intervals": [], + }, + "lifting_network":{ + "active":False, + #"model":"decoder_model", + "model":{ + "num_levels": "VARIABLE", + "level_scale_factor": 2, + + "input_levels": 1, + "create_inputs": True, + "input_blocks": ["C:1-1"], + #"input_conv_filters": 1, + #"input_conv_kernel_size": 1, + "share_input_layer": False, + + "down_mode": "STRIDED", # STRIDED, NONE + "down_conv_filters": None, + "down_conv_kernel_size": 4, + "share_down_layer": True, + + "encoder_resblocks": ["RB:1-1_1-1"], + "share_encoder": False, + + "decoder_resblocks": ["RB:1-1_1-1"], + "share_decoder": False, + + "up_mode": "NNSAMPLE_CONV", + "up_conv_filters": 1, + "up_conv_kernel_size": 4, + "share_up_layer": True, + + "skip_merge_mode": "CONCAT", # CONCAT, WSUM, SUM + + #"output_levels": 1, + "output_blocks": None, + "output_channels": 1, + "share_output_layer": True, + "output_activation": "none", # none, relu, lrelu + "output_conv_kernel_size": 1, + "output_mode": "SINGLE", # SINGLE, RESIDUAL + # global settings + "conv_activation": "relu", # none, relu, lrelu + "alpha": 0.2, # leak for lrelu + "conv_padding": "ZERO", # ZERO, MIRRIR + "normalization": "LAYER", # NONE, LAYER + }, # or path to model file + "start_level": 0, + "min_grid_res": 8, + "train_mode": "ALL", #"ALL", "TOP", "TOP_DEC" + "train_mode_schedule": sSchedule.setup_boolean_schedule(start=True, offset=0), + "skip_merge_weight_schedule": sSchedule.setup_linear_schedule_2(start=1., end=1.0, steps=650, offset=250), + "grow_intervals": [], + }, + "frame_merge_network":{ + "active":False, + #"model":"decoder_model", + "model":{ + "num_levels": "VARIABLE", + "level_scale_factor": 2, + + "input_levels": 1, + "create_inputs": True, + "input_blocks": ["C:1-1"], + #"input_conv_filters": 1, + #"input_conv_kernel_size": 1, + "share_input_layer": False, + + "down_mode": "STRIDED", # STRIDED, NONE + "down_conv_filters": None, + "down_conv_kernel_size": 4, + "share_down_layer": True, + + "encoder_resblocks": ["RB:1-1_1-1"], + "share_encoder": False, + + "decoder_resblocks": ["RB:1-1_1-1"], + "share_decoder": False, + + "up_mode": "NNSAMPLE_CONV", + "up_conv_filters": 1, + "up_conv_kernel_size": 4, + "share_up_layer": True, + + "skip_merge_mode": "CONCAT", # CONCAT, WSUM, SUM + + #"output_levels": 1, + "output_blocks": None, + "share_output_layer": True, + "output_activation": "none", # none, relu, lrelu + "output_conv_kernel_size": 1, + "output_mode": "SINGLE", # SINGLE, RESIDUAL + # global settings + "conv_activation": "relu", # none, relu, lrelu + "alpha": 0.2, # leak for lrelu + "conv_padding": "ZERO", # ZERO, MIRRIR + "normalization": "LAYER", # NONE, LAYER + }, # or path to model file + "start_level": 0, + "min_grid_res": 8, + "train_mode": "ALL", #"ALL", "TOP", "TOP_DEC" + "train_mode_schedule": sSchedule.setup_boolean_schedule(start=True, offset=0), + "skip_merge_weight_schedule": sSchedule.setup_linear_schedule_2(start=1., end=1.0, steps=650, offset=250), + "grow_intervals": [], + }, + 'train_frame_encoders': sSchedule.setup_constant_schedule(start=True), + "density":{ + "decoder":{ + "active":True, + # 'layers':[4,8,16,8,4], + # 'stride':1, + # 'skip_connections':None, + # 'skip_mode':"CONCAT", + # 'kernel_size':4, + # 'padding':'MIRROR', #NONE(=valid), ZERO(=same), MIRROR(=tf.pad(REFLECT)) + # 'activation':'lrelu', # relu or lrelu + # 'activation_alpha':0.2, # leak for lrelu + 'input_type':"PREPROC", #use raw or preproc images + #"model":"decoder_model", #3D features to density + "model":{ + #"dimension": 2, + "num_levels": "VARIABLE", + "level_scale_factor": 2, + + #"input_channels": 3, + "input_levels": 1, + "create_inputs": True, + "input_blocks": ["C:1-1"], + #"input_conv_filters": 1, + #"input_conv_kernel_size": 1, + "share_input_layer": False, + + "down_mode": "STRIDED", # STRIDED, NONE + "down_conv_filters": None, + "down_conv_kernel_size": 4, + "share_down_layer": True, + + "encoder_resblocks": ["RB:1-1_1-1"], + "share_encoder": False, + + "decoder_resblocks": ["RB:1-1_1-1"], + "share_decoder": False, + + "up_mode": "NNSAMPLE_CONV", + "up_conv_filters": 1, + "up_conv_kernel_size": 4, + "share_up_layer": True, + + "skip_merge_mode": "CONCAT", # CONCAT, WSUM, SUM + + #"output_levels": 1, + "output_blocks": None, + #"output_channels": 1, + "share_output_layer": True, + "output_activation": "none", # none, relu, lrelu + "output_conv_kernel_size": 1, + "output_mode": "SINGLE", # SINGLE, RESIDUAL + # global settings + "conv_activation": "relu", # none, relu, lrelu + "alpha": 0.2, # leak for lrelu + "conv_padding": "ZERO", # ZERO, MIRRIR + "normalization": "LAYER", # NONE, LAYER + }, # or path to model file + + "start_level": 0, + "min_grid_res": 10, + "train_mode": "ALL", #"ALL", "TOP", "TOP_DEC" + "train_mode_schedule": sSchedule.setup_boolean_schedule(start=True, offset=0), + # network inputs + "base_input": "ZERO", #ZERO TARGET_HULL + "step_input_density": [], #0 is the current frame, 1 the next, etc. can be negative + "step_input_density_target": [], #0 is the current frame, 1 the next, etc. can be negative + "step_input_features": [0,1], + "type_input_features": ["TARGET_UNPROJECTION"], # case-sensitive, order-invariant. TARGET_UNPROJECTION, TARGET_HULL + "warp_input_indices": [0], #indices of network inputs to be warped. first the the density inputs, then the elements of step_input_features + + "recursive_MS": False, #do recursion and growing in NeuralGrid instead of UNet/Model. evey level uses the full specified model, potetially with multiple levels. + "recursive_MS_levels": "VARIABLE", + "recursive_MS_residual": True, + "recursive_MS_direct_input": False, #if false: generate density decoder input at highest scale/resolution and sample down. if true: generate input at resolution required for current scale. + "recursive_MS_scale_factor": 2, + "recursive_MS_shared_model": True, + "recursive_MS_train_mode": "ALL", #ALL, TOP + "skip_merge_weight_schedule": sSchedule.setup_linear_schedule_2(start=1., end=1.0, steps=650, offset=250), + "grow_intervals": [], + "recursive_MS_copy_on_grow": True, + + "base_SDF_mode": "NONE", # "NONE", "RESIDUAL", "INPUT_RESIDUAL" + }, + 'pre_optimization': True, + 'pre_opt':{ + 'first':{ + 'iterations':0, + }, + 'iterations':2000, + 'learning_rate':{'type':'exponential', 'max':4e-5, 'min': 1e-7, 'start':4e-5, 'base':0.5, 'scale':1/2000, 'offset':2000}, + 'raw_target_loss':1.0, + 'discriminator_loss': sSchedule.setup_linear_schedule_2(start=1e-6, end=5e-5, steps=6500) , + 'grow':{ + "factor":2, + 'intervals':[], #6000 + }, + }, + 'train_decoder': sSchedule.setup_constant_schedule(start=True), + }, + "velocity":{ + "decoder":{ + "active":False, + # 'layers':[4,8,16,8,4], + # 'stride':1, + # 'skip_connections':None, + # 'skip_mode':"CONCAT", + # 'kernel_size':4, + # 'padding':'MIRROR', #NONE(=valid), ZERO(=same), MIRROR(=tf.pad(REFLECT)) + # 'activation':'lrelu', # relu or lrelu + # 'activation_alpha':0.2, # leak for lrelu + 'input_type':"PREPROC", #use raw or preproc images + 'velocity_format': "CENTERED", #CENTERED, STAGGERED + "model": { + #"dimension": 2, + "num_levels": "VARIABLE", + "level_scale_factor": 2, + + #"input_channels": 3, + "input_levels": 1, + "create_inputs": True, + "input_blocks": ["C:1-1"], + #"input_conv_filters": 1, + #"input_conv_kernel_size": 1, + "share_input_layer": False, + + "down_mode": "STRIDED", # STRIDED, NONE + "down_conv_filters": None, + "down_conv_kernel_size": 4, + "share_down_layer": True, + + "encoder_resblocks": ["RB:1-1_1-1"], + "share_encoder": False, + + "decoder_resblocks": ["RB:1-1_1-1"], + "share_decoder": False, + + "up_mode": "NNSAMPLE_CONV", + "up_conv_filters": 1, + "up_conv_kernel_size": 4, + "share_up_layer": True, + + "skip_merge_mode": "CONCAT", # CONCAT, WSUM, SUM + + #"output_levels": 1, + "output_blocks": None, + #"output_channels": 1, + "share_output_layer": True, + "output_activation": "none", # none, relu, lrelu + "output_conv_kernel_size": 1, + "output_mode": "SINGLE", # SINGLE, RESIDUAL + # global settings + "conv_activation": "relu", # none, relu, lrelu + "alpha": 0.2, # leak for lrelu + "conv_padding": "ZERO", # ZERO, MIRRIR + "normalization": "LAYER", # NONE, LAYER + }, #3D features to velocity + "start_level": 0, + "min_grid_res": 10, + "train_mode": "ALL", #"ALL", "TOP", "TOP_DEC" + "train_mode_schedule": sSchedule.setup_boolean_schedule(start=True, offset=0), + # network inputs + "step_input_density": [], #0 is the current frame, 1 the next, etc. can be negative + "step_input_density_target": [], #0 is the current frame, 1 the next, etc. can be negative + "step_input_density_proxy": [], #0 is the current frame, 1 the next, etc. can be negative + "step_input_features": [0,1], + "type_input_features": ["TARGET_UNPROJECTION"], # case-sensitive, order-invariant. TARGET_UNPROJECTION, TARGET_HULL + "downscale_input_modes": ["RESAMPLE", "RESAMPLE"], # + "warp_input_indices": [0], #indices of network inputs to be warped. first the the density inputs, then the elements of step_input_features + + "share_downscale_encoder": False, + + "recursive_MS": False, #do recursion and growing in NeuralGrid instead of UNet/Model. evey level uses the full specified model, potetially with multiple levels. + "recursive_MS_levels": "VARIABLE", + "recursive_MS_direct_input": False, #if false: generate density decoder input at highest scale/resolution and sample down. if true: generate input at resolution required for current scale. + "recursive_MS_use_max_level_input": False, + "recursive_MS_scale_factor": 2, + "recursive_MS_shared_model": True, + "recursive_MS_train_mode": "ALL", #ALL, TOP + "recursive_MS_residual_weight": None, + "skip_merge_weight_schedule": sSchedule.setup_linear_schedule_2(start=1., end=1.0, steps=650, offset=250), + "grow_intervals": [], + "recursive_MS_copy_on_grow": True, + # "load":None, #path to model file + }, + 'pre_optimization': False, + 'train_decoder': sSchedule.setup_constant_schedule(start=True), + }, + }, + 'validation':{ + 'input_view_mask':[1,3], + 'simulation':80, + 'start':80, + 'stop':141, + 'step':50, + 'batch_size':2, + 'warp_test':[], + 'warp_test_render':False, + 'render_cycle':False, + 'render_cycle_steps':8, + 'render_density':True, + 'render_shadow':True, + 'render_target':True, + 'render_velocity':False, + # 'sequence_step':1, + # 'sequence_length':1, + }, +} +RECONSTRUCT_SEQUENCE_SETUP_NEURAL_DENSITY = _CSETUPS_update_dict_recursive(RECONSTRUCT_SEQUENCE_SETUP_BASE, RECONSTRUCT_SEQUENCE_SETUP_NEURAL_DENSITY, deepcopy=True, new_key='KEEP') + +RECONSTRUCT_SEQUENCE_SETUP_NEURAL_VELOCITY = { + "desc":"train neural networks to generate velocity directly from consecutive densities.", + "data":{ + 'grid_size':48, #128, #resolution + 'y_scale': 2, # "SF" ~1.77 (16/9) + 'clip_grid':False, + 'clip_grid_pad':4, + 'crop_grid':True, + 'res_down_factor':128*5, #target data down scale factor is 6 (i.e. 1/6 of raw image resolution) at 128 base grid resolution + + 'start':20,#56,#40, + 'stop':141,#68,#52, #exclusive + 'step':2,#2, + # + "randomize":64, + "batch_size":4, + "sims":list(range(20)), + "sequence_step":1, + "sequence_length":2, + "density":{ + 'target_type': "PREPROC", #RAW, PREPROC, SYNTHETIC + 'scale':1, + 'inflow':{ + 'active':False, + 'hull_height':4, + 'height':'MAX', + }, + }, + }, + "training":{ + 'iterations':0, + "view_encoder":{ + #"active":True, active= density.decoder.active or velocity.decoder.active + 'encoder': ['L'], #NETWORK, NONE + # 'layers':[4,8,16,16,16,4], + # 'stride':1, + # 'skip_connections':None, + # 'skip_mode':"CONCAT", + # 'kernel_size':4, + # 'padding':'MIRROR', #NONE(=valid), ZERO(=same), MIRROR(=tf.pad(REFLECT)) + # 'activation':'lrelu', # relu or lrelu + # 'activation_alpha':0.2, # leak for lrelu + # "load_encoder":None, + "lifting":"UNPROJECT", #UNPROJECT ;2D->3D method + #"load_lifting":None, #lifting could be NN + "merge":"MEAN", # SUM, MEAN ;method to merge 3D embeddings of multiple views + #"load_merge":None, #merging could be NN + + "model":{ + "num_levels": "VARIABLE", + "level_scale_factor": 2, + + "input_levels": 1, + "create_inputs": True, + "input_blocks": ["C:1-1"], + #"input_conv_filters": 1, + #"input_conv_kernel_size": 1, + "share_input_layer": False, + + "down_mode": "STRIDED", # STRIDED, NONE + "down_conv_filters": None, + "down_conv_kernel_size": 4, + "share_down_layer": True, + + "encoder_resblocks": ["RB:1-1_1-1"], + "share_encoder": False, + + "decoder_resblocks": ["RB:1-1_1-1"], + "share_decoder": False, + + "up_mode": "NNSAMPLE_CONV", + "up_conv_filters": 1, + "up_conv_kernel_size": 4, + "share_up_layer": True, + + "skip_merge_mode": "CONCAT", # CONCAT, WSUM, SUM + + #"output_levels": 1, + "output_blocks": None, + "output_channels": 1, + "share_output_layer": True, + "output_activation": "none", # none, relu, lrelu + "output_conv_kernel_size": 1, + "output_mode": "SINGLE", # SINGLE, RESIDUAL + # global settings + "conv_activation": "relu", # none, relu, lrelu + "alpha": 0.2, # leak for lrelu + "conv_padding": "ZERO", # ZERO, MIRRIR + "normalization": "LAYER", # NONE, LAYER + }, # or path to model file + "start_level": 0, + "train_mode": "ALL", #"ALL", "TOP", "TOP_DEC" + "train_mode_schedule": sSchedule.setup_boolean_schedule(start=True, offset=0), + "skip_merge_weight_schedule": sSchedule.setup_linear_schedule_2(start=1., end=1.0, steps=650, offset=250), + "grow_intervals": [], + }, + "volume_encoder":{ + "active":False, + #"model":"decoder_model", + "model":{ + "num_levels": "VARIABLE", + "level_scale_factor": 2, + + "input_levels": 1, + "create_inputs": True, + "input_blocks": ["C:1-1"], + #"input_conv_filters": 1, + #"input_conv_kernel_size": 1, + "share_input_layer": False, + + "down_mode": "STRIDED", # STRIDED, NONE + "down_conv_filters": None, + "down_conv_kernel_size": 4, + "share_down_layer": True, + + "encoder_resblocks": ["RB:1-1_1-1"], + "share_encoder": False, + + "decoder_resblocks": ["RB:1-1_1-1"], + "share_decoder": False, + + "up_mode": "NNSAMPLE_CONV", + "up_conv_filters": 1, + "up_conv_kernel_size": 4, + "share_up_layer": True, + + "skip_merge_mode": "CONCAT", # CONCAT, WSUM, SUM + + #"output_levels": 1, + "output_blocks": None, + "output_channels": 1, + "share_output_layer": True, + "output_activation": "none", # none, relu, lrelu + "output_conv_kernel_size": 1, + "output_mode": "SINGLE", # SINGLE, RESIDUAL + # global settings + "conv_activation": "relu", # none, relu, lrelu + "alpha": 0.2, # leak for lrelu + "conv_padding": "ZERO", # ZERO, MIRRIR + "normalization": "LAYER", # NONE, LAYER + }, # or path to model file + }, + "lifting_network":{ + "active":False, + #"model":"decoder_model", + "model":{ + "num_levels": "VARIABLE", + "level_scale_factor": 2, + + "input_levels": 1, + "create_inputs": True, + "input_blocks": ["C:1-1"], + #"input_conv_filters": 1, + #"input_conv_kernel_size": 1, + "share_input_layer": False, + + "down_mode": "STRIDED", # STRIDED, NONE + "down_conv_filters": None, + "down_conv_kernel_size": 4, + "share_down_layer": True, + + "encoder_resblocks": ["RB:1-1_1-1"], + "share_encoder": False, + + "decoder_resblocks": ["RB:1-1_1-1"], + "share_decoder": False, + + "up_mode": "NNSAMPLE_CONV", + "up_conv_filters": 1, + "up_conv_kernel_size": 4, + "share_up_layer": True, + + "skip_merge_mode": "CONCAT", # CONCAT, WSUM, SUM + + #"output_levels": 1, + "output_blocks": None, + "output_channels": 1, + "share_output_layer": True, + "output_activation": "none", # none, relu, lrelu + "output_conv_kernel_size": 1, + "output_mode": "SINGLE", # SINGLE, RESIDUAL + # global settings + "conv_activation": "relu", # none, relu, lrelu + "alpha": 0.2, # leak for lrelu + "conv_padding": "ZERO", # ZERO, MIRRIR + "normalization": "LAYER", # NONE, LAYER + }, # or path to model file + "start_level": 0, + "min_grid_res": 8, + "train_mode": "ALL", #"ALL", "TOP", "TOP_DEC" + "train_mode_schedule": sSchedule.setup_boolean_schedule(start=True, offset=0), + "skip_merge_weight_schedule": sSchedule.setup_linear_schedule_2(start=1., end=1.0, steps=650, offset=250), + "grow_intervals": [], + }, + "frame_merge_network":{ + "active":False, + #"model":"decoder_model", + "model":{ + "num_levels": "VARIABLE", + "level_scale_factor": 2, + + "input_levels": 1, + "create_inputs": True, + "input_blocks": ["C:1-1"], + #"input_conv_filters": 1, + #"input_conv_kernel_size": 1, + "share_input_layer": False, + + "down_mode": "STRIDED", # STRIDED, NONE + "down_conv_filters": None, + "down_conv_kernel_size": 4, + "share_down_layer": True, + + "encoder_resblocks": ["RB:1-1_1-1"], + "share_encoder": False, + + "decoder_resblocks": ["RB:1-1_1-1"], + "share_decoder": False, + + "up_mode": "NNSAMPLE_CONV", + "up_conv_filters": 1, + "up_conv_kernel_size": 4, + "share_up_layer": True, + + "skip_merge_mode": "CONCAT", # CONCAT, WSUM, SUM + + #"output_levels": 1, + "output_blocks": None, + "share_output_layer": True, + "output_activation": "none", # none, relu, lrelu + "output_conv_kernel_size": 1, + "output_mode": "SINGLE", # SINGLE, RESIDUAL + # global settings + "conv_activation": "relu", # none, relu, lrelu + "alpha": 0.2, # leak for lrelu + "conv_padding": "ZERO", # ZERO, MIRRIR + "normalization": "LAYER", # NONE, LAYER + }, # or path to model file + "start_level": 0, + "train_mode": "ALL", #"ALL", "TOP", "TOP_DEC" + "train_mode_schedule": sSchedule.setup_boolean_schedule(start=True, offset=0), + "skip_merge_weight_schedule": sSchedule.setup_linear_schedule_2(start=1., end=1.0, steps=650, offset=250), + "grow_intervals": [], + }, + "density":{ + "decoder":{ + "active":True, + 'input_type':"PREPROC", #use raw or preproc images + "model": "[RUNID:210305-172218]density_decoder", # or path to model file + "start_level": 0, + "min_grid_res": 3, + "skip_merge_weight_schedule": sSchedule.setup_linear_schedule_2(start=1., end=1.0, steps=650, offset=250), + "grow_intervals": [], + }, + 'pre_optimization': False, + }, + "velocity":{ + "decoder":{ + "active":True, + 'input_type':"PREPROC", #use raw or preproc images + 'velocity_format': "CENTERED", #CENTERED, STAGGERED + "model": { + #"dimension": 2, + "num_levels": "VARIABLE", + "level_scale_factor": 2, + + #"input_channels": 3, + "input_levels": -1, + "create_inputs": True, + "input_blocks": ["C:16-1"], + #"input_conv_filters": 1, + #"input_conv_kernel_size": 1, + "share_input_layer": True, + + "down_mode": "NONE", # STRIDED, NONE + "down_conv_filters": None, + "down_conv_kernel_size": 4, + "share_down_layer": True, + + "encoder_resblocks": ["RB:8_16_s0","RB:8_16_s0","RB:8_16_s0","RB:8_16_s0"], + "share_encoder": True, + + "decoder_resblocks": ["RB:8_16","RB:8_16_s0","RB:8_16_s0","RB:8_16_s0"], + "share_decoder": True, + + "up_mode": "NNSAMPLE_CONV", + "up_conv_filters": 16, + "up_conv_kernel_size": 4, + "share_up_layer": True, + + "skip_merge_mode": "CONCAT", # CONCAT, WSUM, SUM + + #"output_levels": 1, + "output_blocks": ["RB:8_16_s0","RB:8_16_s0","RB:8_16_s0","RB:8_16_s0","RB:8_16_s0"], + #"output_channels": 1, + "share_output_layer": True, + "output_activation": "none", # none, relu, lrelu + "output_conv_kernel_size": 1, + "output_mode": "SINGLE", # SINGLE, RESIDUAL + # global settings + "conv_activation": "relu", # none, relu, lrelu + "alpha": 0.2, # leak for lrelu + "conv_padding": "ZERO", # ZERO, MIRRIR + "normalization": "LAYER", # NONE, LAYER + }, #3D features to velocity + "start_level": 0, + "min_grid_res": 3, + "skip_merge_weight_schedule": sSchedule.setup_linear_schedule_2(start=1., end=1.0, steps=650, offset=250), + # network inputs + "step_input_density": [], #0 is the current frame, 1 the next, etc. can be negative + "step_input_density_target": [], #0 is the current frame, 1 the next, etc. can be negative + "step_input_density_proxy": [], #0 is the current frame, 1 the next, etc. can be negative + "step_input_features": [0,1], + "type_input_features": ["TARGET_UNPROJECTION"], + "downscale_input_modes": ["RESAMPLE", "RESAMPLE"], # + "warp_input_indices": [0], #indices of network inputs to be warped. first the the density inputs, then the elements of step_input_features + + + + "recursive_MS": False, #do recursion and growing in NeuralGrid instead of UNet/Model. evey level uses the full specified model, potetially with multiple levels. + "recursive_MS_levels": "VARIABLE", + "recursive_MS_direct_input": False, #if false: generate density decoder input at highest scale/resolution and sample down. if true: generate input at resolution required for current scale. + "recursive_MS_use_max_level_input": False, + "recursive_MS_scale_factor": 2, + "recursive_MS_shared_model": True, + "recursive_MS_train_mode": "ALL", #ALL, TOP + "recursive_MS_residual_weight": None, + "grow_intervals": [1200,1400,1600,1800], + "recursive_MS_copy_on_grow": True, + "train_mode": "ALL", #"ALL", "TOP", "TOP_DEC" + "train_mode_schedule": sSchedule.setup_boolean_schedule(start=True, offset=0), + }, + 'pre_optimization': True, + 'pre_opt':{ + 'first':{ #settings for first frame + 'iterations':16000,#10000, #30000, + 'learning_rate':{'type':'exponential', 'max':1e-5, 'start':1e-5, 'base':0.5, 'scale':1/2000, 'offset':0}, + + 'density_warp_loss':1, + 'velocity_warp_loss':0.0, + 'smoothness_loss':0.0, + 'smoothness_neighbours':3, + 'cossim_loss':0.0, + 'divergence_loss': sSchedule.setup_linear_schedule_2(start=0.0,end=2.0,steps=5000,offset=1000), + 'magnitude_loss':{'type':'exponential', 'start':2e-2, 'base':0.5, 'scale':1/2000}, + 'CFL_loss':0.0, + 'MS_coherence_loss':0.0, + 'regularization':0.0001, + 'grow':{ + "factor":2, + "scale_magnitude":False, + 'intervals':[1200,1400,1600,1800], #6000 + }, + + }, + }, + }, + }, + 'validation':{ + 'input_view_mask':[1,3], + 'simulation':80, + 'start':80, + 'stop':142, + 'step':50, + 'batch_size':2, + 'warp_test':[], + 'warp_test_render':False, + 'render_cycle':True, + 'render_cycle_steps':8, + 'render_density':True, + 'render_shadow':False, + 'render_target':False, + 'render_velocity':True, + # 'sequence_step':1, + # 'sequence_length':1, + }, +} +RECONSTRUCT_SEQUENCE_SETUP_NEURAL_VELOCITY = _CSETUPS_update_dict_recursive(RECONSTRUCT_SEQUENCE_SETUP_BASE, RECONSTRUCT_SEQUENCE_SETUP_NEURAL_VELOCITY, deepcopy=True, new_key='KEEP') + +#RECONSTRUCT_SEQUENCE_SETUP_NEURAL = TODO + + +def reconstruct_sequence_setup_compatibility(setup, log_func=lambda s, *p: None): + ''' + backwards compat for changes in the configuration + ''' + setup = copy.deepcopy(setup) + setup = munch.munchify(setup) + # changed/replaced keys + def log_key_update(k1, v1, k2, v2): + log_func("Update old key '%s' with value '%s' to '%s' with value '%s'", k1, v1, k2, v2) + # target type + if "synthetic_target" in setup.data.density and not "target_type" in setup.data.density: + setup.data.density.target_type = "SYNTHETIC" if setup.data.density.synthetic_target else "RAW" + log_key_update('setup.data.density.synthetic_target', setup.data.density.synthetic_target, 'setup.data.density.target_type', setup.data.density.target_type) + del setup.data.density.synthetic_target + + # adjustments for mechanical changes + # if using data from before the sampling step correction: multiply density with 256, warn about loss and shadow/light scaling + +# old setups \ No newline at end of file diff --git a/compile.py b/compile.py new file mode 100644 index 0000000..bb0b580 --- /dev/null +++ b/compile.py @@ -0,0 +1,83 @@ + +import subprocess, datetime +import os, shutil + +def compile(file_name, compile_cuda=True, create_backup=True): + srcPath = os.path.abspath("./phitest/render/cuda/src") + buildPath = os.path.abspath("./phitest/render/cuda/build") + #buildPath = os.path.abspath("./phitest/render/cuda/test_build") + now = datetime.datetime.now() + now_str = now.strftime("%y%m%d-%H%M%S") + bpkPath = os.path.join(os.path.abspath("./phitest/render/cuda/build_bkp"), now_str + "_" + file_name) + print("Source Path:\t" + srcPath) + print("Build Path:\t" + buildPath) + + # Get TF Compile/Link Flags and write to env + import tensorflow as tf + print('tensorflow version', tf.__version__) + TF_CFLAGS = tf.sysconfig.get_compile_flags() + TF_CFLAGS += ['-I'+os.path.abspath('./lib/glm')] + #TF_CFLAGS += ['-I'+os.path.abspath('./lib/own')] + TF_LFLAGS = tf.sysconfig.get_link_flags() + + print('compile flags:', TF_CFLAGS) + print('link flags:', TF_LFLAGS) + + + # Remove old build files + if os.path.isdir(buildPath): + build_dir = list(os.listdir(buildPath)) + # Backup old build files + if create_backup: + print("Create backup for '%s' in '%s'" %(file_name, bpkPath)) + os.makedirs(bpkPath) + for f in build_dir: + if file_name in f: + shutil.copy2(os.path.join(buildPath, f), os.path.join(bpkPath, f)) + print("Removing old build files from %s" % buildPath) + for f in build_dir: + if file_name in f and (compile_cuda or '.cu.o' not in f): + os.remove(os.path.join(buildPath, f)) + else: + print("Creating build directory at %s" % buildPath) + os.mkdir(buildPath) + + print("Compiling CUDA code...") + # Build the Laplace Matrix Generation CUDA Kernels + + if compile_cuda: + subprocess.check_call(['nvcc', + "-std=c++11", + "-c", + "-o", + os.path.join(buildPath, file_name+'.cu.o'), + os.path.join(srcPath, file_name+'.cu.cc'), + "-D GOOGLE_CUDA=1", + "-x", + "cu", + "-Xcompiler", + "-fPIC", + "-I/usr/local/cuda-9.2/samples/common/inc"] + + TF_CFLAGS) + + subprocess.check_call(['gcc', + "-std=c++11", + "-shared", + "-o", + os.path.join(buildPath, file_name+'.so'), + os.path.join(srcPath, file_name+'.cc'), + os.path.join(buildPath, file_name+'.cu.o'), + "-D GOOGLE_CUDA=1", + "-fPIC",] + + TF_CFLAGS + TF_LFLAGS) + +if __name__=='__main__': + bkp = input("Backup previous build? [Y/n]:").lower() + if bkp in ["", "y", "yes", "t", "true", "1"]: + bkp = True; + elif bkp in ["n", "no", "f", "false", "0"]: + bkp = False; + compile('resample_grid', create_backup=bkp) + compile('raymarch_grid', create_backup=bkp) + compile('reduce_blend', create_backup=bkp) + compile('advect', create_backup=bkp) diff --git a/configs/setup_density.json b/configs/setup_density.json new file mode 100644 index 0000000..e7cff29 --- /dev/null +++ b/configs/setup_density.json @@ -0,0 +1,1346 @@ +{ + "data": { + "MS_images": false, + "MS_volume": false, + "SDF": false, + "batch_size": [ + 4, + -2 + ], + "clip_grid": false, + "clip_grid_pad": 4, + "crop_grid": true, + "density": { + "density_type": "SF", + "hull_image_blur_std": 1.0, + "hull_smooth_blur_std": 0.0, + "hull_threshold": 0.04, + "hull_volume_blur_std": 0.5, + "inflow": { + "active": false, + "height": "MAX", + "hull_height": 4 + }, + "initial_value": "", + "max": 255.0, + "min": 0.0, + "render_targets": false, + "scalarFlow_reconstruction": "", + "scale": 1, + "synthetic_target_density_scale": 1.0, + "target": "data/scalarFlow/sim_{sim:06d}/input/cam/imgsUnproc_{frame:06d}.npz", + "target_cam_ids": "ALL", + "target_flip_y": false, + "target_preproc": "data/scalarFlow/sim_{sim:06d}/input/postprocessed/imgs_{frame:06d}.npz", + "target_scale": 1.5, + "target_threshold": 0.04, + "target_type": "PREPROC" + }, + "discriminator": { + "crop_size": [ + 96, + 96 + ], + "density_type": "SF", + "frames": [ + 35, + 125, + 1 + ], + "gamma_fake": [ + 0.5, + 2 + ], + "gamma_real": [ + 0.5, + 2 + ], + "initial_value": "", + "real_res_down": 4, + "render_targets": false, + "rotation_mode": "90", + "scale_fake": [ + 0.7, + 1.4 + ], + "scale_input_to_crop": false, + "scale_range": [ + 0.85, + 1.15 + ], + "scale_real": [ + 0.8, + 1.8 + ], + "scale_real_to_cam": true, + "simulations": [ + 0, + 20 + ], + "target": "data/scalarFlow/sim_{sim:06d}/input/cam/imgsUnproc_{frame:06d}.npz", + "target_preproc": "data/scalarFlow/sim_{sim:06d}/input/postprocessed/imgs_{frame:06d}.npz", + "target_type": "RAW" + }, + "grid_size": 64, + "hull": "TARGETS", + "initial_buoyancy": [ + 0.0, + 0.0, + 0.0 + ], + "load_sequence": null, + "load_sequence_pre_opt": false, + "rand_seed_global": 460585320, + "randomize": 64, + "res_down_factor": 640, + "resource_device": "/cpu:0", + "run_dirs": [ + "runs/train_velDens_sequence" + ], + "scalarFlow_frame_offset": -11, + "sequence_length": 1, + "sequence_step": [ + 1, + 2, + 3, + 4 + ], + "sims": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19 + ], + "simulation": 0, + "start": 20, + "step": 2, + "stop": 141, + "synth_shapes": { + "active": false, + "max_translation": 0.8, + "shape_types": [ + 0, + 1 + ] + }, + "velocity": { + "boundary": "CLAMP", + "init_mask": "HULL_TIGHT_NEXT", + "init_std": 0.1, + "initial_value": "data/scalarFlow/sim_{sim:06d}/reconstruction/velocity_{frame:06d}.npz", + "load_step": 1, + "scalarFlow_reconstruction": "data/scalarFlow/sim_{sim:06d}/reconstruction/velocity_{frame:06d}.npz" + }, + "y_scale": 1.5 + }, + "debug": { + "disc_dump_samples": false, + "print_weight_grad_stats": false, + "target_dump_samples": false + }, + "desc": "Sample setup for pre-training of the density estimator with ScalarFlow data.", + "paths": { + "base": "runs", + "group": "train_velDens_sequence" + }, + "rendering": { + "SDF_threshold": 0.02, + "allow_fused_rendering": true, + "allow_static_cameras": false, + "background": { + "color": [ + 0, + 0.5, + 1.0 + ], + "type": "COLOR" + }, + "blend_mode": "BEER_LAMBERT", + "boundary": "BORDER", + "filter_mode": "LINEAR", + "lighting": { + "ambient_intensity": 0.64, + "initial_intensity": 0.85, + "shadow_resolution": [ + 128, + 96, + 96 + ] + }, + "luma": [ + 0.2126, + 0.7152, + 0.0722 + ], + "main_camera": { + "base_resolution": [ + 256, + 1920, + 1080 + ], + "distance": 0.8, + "far": 1.3, + "fov": 40, + "near": 0.3, + "resolution_scale": 0.3333333333333333 + }, + "mip": { + "bias": 0.0, + "level": 4, + "mode": "LINEAR" + }, + "monochrome": false, + "num_images": 24, + "sample_gradients": true, + "steps_per_cycle": 24, + "synthetic_target": { + "ambient_intensity": 0.64, + "blend_mode": "BEER_LAMBERT", + "filter_mode": "LINEAR", + "initial_intensity": 0.85 + }, + "target_cameras": { + "calibration_file": "scalaFlow_cameras.json", + "camera_ids": [ + 2, + 1, + 0, + 4, + 3 + ], + "crop_frustum": false, + "crop_frustum_pad": 2 + }, + "velocity_scale": 1024 + }, + "title": "density_ScalarFlow", + "training": { + "MS_weighting": null, + "allow_MS_losses": false, + "checkpoint_interval": 250, + "density": { + "SDF_pos_loss": 0, + "camera_jitter": false, + "center_loss": 0.001, + "decoder": { + "active": true, + "base_SDF_mode": "NONE", + "base_input": "ZERO", + "grow_intervals": [], + "input_type": "PREPROC", + "min_grid_res": 4, + "model": { + "alpha": 0.2, + "conv_activation": "relu", + "conv_padding": "ZERO", + "create_inputs": false, + "decoder_resblocks": [], + "down_conv_filters": null, + "down_conv_kernel_size": 4, + "down_mode": "NONE", + "encoder_resblocks": [], + "input_blocks": [], + "input_levels": 1, + "level_scale_factor": 2.0, + "normalization": "LAYER", + "num_levels": 1, + "output_activation": "none", + "output_blocks": [], + "output_conv_kernel_size": 0, + "output_mode": "SINGLE", + "share_decoder": false, + "share_down_layer": true, + "share_encoder": true, + "share_input_layer": true, + "share_output_layer": true, + "share_up_layer": true, + "skip_merge_mode": "CONCAT", + "up_conv_filters": 16, + "up_conv_kernel_size": 4, + "up_mode": "NNSAMPLE_CONV" + }, + "recursive_MS": false, + "recursive_MS_copy_on_grow": false, + "recursive_MS_direct_input": false, + "recursive_MS_levels": 1, + "recursive_MS_residual": true, + "recursive_MS_scale_factor": 2.0, + "recursive_MS_shared_model": true, + "recursive_MS_train_mode": "ALL", + "skip_merge_weight_schedule": { + "base": 2.0, + "max": 1.0, + "min": 1.0, + "offset": 250, + "scale": 1.0, + "schedule": [], + "start": 1.0, + "step": 0.0, + "type": "LINEAR" + }, + "start_level": 0, + "step_input_density": [], + "step_input_density_target": [], + "step_input_features": [ + 0 + ], + "train_mode": "ALL", + "train_mode_schedule": { + "base": 2.0, + "max": Infinity, + "min": -Infinity, + "offset": 0, + "scale": 1.0, + "schedule": [], + "start": true, + "step": 0, + "type": "BOOLEAN" + }, + "type_input_features": [ + "ENC3D" + ], + "warp_input_indices": [ + 0 + ] + }, + "discriminator_loss": 0.0002, + "error_functions": { + "SDF_pos_loss": "AE", + "center_loss": "SE", + "hull": "SE", + "negative": "SE", + "preprocessed_target_loss": "SE", + "raw_target_loss": "SE", + "smoothness_loss": "SE", + "smoothness_loss_2": "SE", + "target_depth_smoothness_loss": "SE", + "temporal_smoothness_loss": "SE", + "volume_proxy_loss": "SE", + "volume_target_loss": "SE", + "warp_loss": "SE" + }, + "grow": { + "factor": 2.0, + "intervals": [ + 8000, + 8000, + 8000 + ], + "post_grow_actions": [], + "pre_grow_actions": [] + }, + "grow_lifting_lr": null, + "grow_lifting_residual": [ + { + "base": 2.0, + "max": Infinity, + "min": -Infinity, + "offset": 0, + "scale": 1.0, + "schedule": [], + "start": 1, + "step": 0, + "type": "CONST" + }, + { + "base": 2.0, + "max": 1, + "min": 0, + "offset": 3000, + "scale": 1.0, + "schedule": [], + "start": 1, + "step": -0.0003333333333333333, + "type": "LINEAR" + }, + { + "base": 2.0, + "max": 1, + "min": 0, + "offset": 22000, + "scale": 1.0, + "schedule": [], + "start": 1, + "step": -0.0003333333333333333, + "type": "LINEAR" + }, + { + "base": 2.0, + "max": 1, + "min": 0, + "offset": 12000, + "scale": 1.0, + "schedule": [], + "start": 1, + "step": -0.0003333333333333333, + "type": "LINEAR" + }, + { + "base": 2.0, + "max": 1, + "min": 0, + "offset": 32000, + "scale": 1.0, + "schedule": [], + "start": 1, + "step": -0.0003333333333333333, + "type": "LINEAR" + } + ], + "grow_lifting_skip": null, + "grow_lifting_train": null, + "hull": 0, + "learning_rate": { + "base": 1.0, + "max": 1, + "min": 0.0, + "offset": -5000, + "scale": 0.0002, + "schedule": [], + "start": 0.0002, + "step": 0, + "type": "ROOT_DECAY" + }, + "main_warp_fwd": true, + "negative": 0.0, + "optim_beta": 0.9, + "pre_opt": { + "SDF_pos_loss": 0.0, + "center_loss": 0.0, + "discriminator_loss": 0.0, + "first": { + "SDF_pos_loss": 0.0, + "center_loss": 0.0, + "discriminator_loss": 0.0, + "hull": 0.0, + "iterations": 0, + "learning_rate": { + "base": 0.5, + "max": Infinity, + "min": -Infinity, + "offset": 0, + "scale": 6.666666666666667e-05, + "schedule": [], + "start": 3.0, + "step": 0, + "type": "EXPONENTIAL" + }, + "negative": 0.0, + "preprocessed_target_loss": 0.0, + "raw_target_loss": 1.74e-05, + "regularization": 0.0001, + "smoothness_loss": 0.0, + "smoothness_loss_2": 0.0, + "smoothness_neighbours": 3, + "target_depth_smoothness_loss": 0.0, + "temporal_smoothness_loss": 0.0, + "volume_target_loss": 0.0, + "warp_loss": 0.0 + }, + "grow": { + "factor": 1.2, + "intervals": [] + }, + "hull": 0.0, + "inspect_gradients": 1, + "iterations": 2400, + "learning_rate": { + "base": 0.5, + "max": Infinity, + "min": -Infinity, + "offset": 0, + "scale": 0.0003333333333333333, + "schedule": [], + "start": 3.0, + "step": 0, + "type": "EXPONENTIAL" + }, + "negative": 0.0, + "preprocessed_target_loss": 0.0, + "raw_target_loss": 1.74e-05, + "regularization": 0.0001, + "seq_init": "WARP", + "smoothness_loss": 0.0, + "smoothness_loss_2": 0.0, + "smoothness_neighbours": 3, + "target_depth_smoothness_loss": 0.0, + "temporal_smoothness_loss": 0.0, + "volume_target_loss": 0.0, + "warp_loss": 0.0 + }, + "pre_optimization": false, + "preprocessed_target_loss": 0.0, + "raw_target_loss": 1, + "regularization": 1e-06, + "scale_render_grads_sharpness": 0.0, + "smoothness_loss": 0, + "smoothness_loss_2": 0, + "smoothness_neighbours": 1, + "target_depth_smoothness_loss": 0.0, + "temporal_smoothness_loss": 0.0, + "train_decoder": true, + "use_hull": true, + "view_interpolation": { + "steps": 0 + }, + "volume_proxy_loss": 0.0, + "volume_target_loss": 0, + "warp_clamp": "MC_SMOOTH", + "warp_gradients": { + "active": true, + "decay": 0.9, + "update_first_only": true, + "weight": 1.0 + }, + "warp_loss": 0.0 + }, + "discriminator": { + "activation": "lrelu", + "activation_alpha": 0.2, + "active": true, + "cam_res_down": 6, + "conditional_hull": false, + "fake_camera_jitter": false, + "grow": { + "factor": 2.0, + "intervals": [] + }, + "history": { + "keep_chance": 0.01, + "load": null, + "reset_on_density_grow": true, + "samples": 4, + "save": false, + "sequence_reuse": true, + "size": 800 + }, + "kernel_size": 4, + "layers": [ + 16, + 16, + 24, + 24, + 32, + 32, + 32, + 64, + 64, + 64, + 16, + 4 + ], + "learning_rate": { + "base": 1.0, + "max": 1, + "min": 0.0, + "offset": -5000, + "scale": 0.0002, + "schedule": [], + "start": 0.0002, + "step": 0, + "type": "ROOT_DECAY" + }, + "loss_type": "RaLSGAN", + "model": null, + "noise_std": 0.0, + "num_fake": 3, + "num_real": 4, + "optim_beta": 0.5, + "padding": "MIRROR", + "pre_opt": { + "first": { + "learning_rate": { + "base": 0.5, + "scale": 0.00013333333333333334, + "start": 0.0004, + "type": "exponential" + }, + "regularization": 0.002, + "train": false + }, + "learning_rate": 0.00016, + "regularization": 0.002, + "train": false + }, + "regularization": 0.002, + "start_delay": 0, + "steps": 1, + "stride": [ + 2, + 1, + 2, + 1, + 2, + 1, + 1, + 2, + 1, + 1, + 1, + 1 + ], + "target_label": 1.0, + "temporal_input": { + "active": false, + "step_range": [ + -3, + 4, + 1 + ] + }, + "train": true, + "use_fc": false + }, + "frame_merge_network": { + "active": false, + "grow_intervals": [], + "min_grid_res": 4, + "model": { + "alpha": 0.2, + "conv_activation": "relu", + "conv_padding": "ZERO", + "create_inputs": true, + "decoder_resblocks": [], + "down_conv_filters": null, + "down_conv_kernel_size": 4, + "down_mode": "STRIDED", + "encoder_resblocks": [ + "RB:64-5_64-5_s0", + "RB:48-5_48-5_s1", + "RB:32-5_32-5_s1" + ], + "input_blocks": [ + "C:64-1" + ], + "input_levels": 1, + "level_scale_factor": 2, + "normalization": "LAYER", + "num_levels": 1, + "output_activation": "none", + "output_blocks": [], + "output_conv_kernel_size": 0, + "output_mode": "SINGLE", + "share_decoder": false, + "share_down_layer": true, + "share_encoder": false, + "share_input_layer": false, + "share_output_layer": true, + "share_up_layer": true, + "skip_merge_mode": "CONCAT", + "up_conv_filters": 1, + "up_conv_kernel_size": 4, + "up_mode": "NNSAMPLE_CONV" + }, + "skip_merge_weight_schedule": { + "base": 2.0, + "max": 1.0, + "min": 1.0, + "offset": 250, + "scale": 1.0, + "schedule": [], + "start": 1.0, + "step": 0.0, + "type": "LINEAR" + }, + "start_level": 0, + "train_mode": "ALL", + "train_mode_schedule": { + "base": 2.0, + "max": Infinity, + "min": -Infinity, + "offset": 0, + "scale": 1.0, + "schedule": [], + "start": true, + "step": 0, + "type": "BOOLEAN" + } + }, + "frame_order": "BWD", + "iterations": 40000, + "lifting_network": { + "active": true, + "grow_intervals": [], + "min_grid_res": 4, + "model": { + "alpha": 0.2, + "conv_activation": "relu", + "conv_padding": "ZERO", + "create_inputs": true, + "decoder_resblocks": [ + [ + "RB:64-5_64-5_s1", + "RB:64-5_64-5_s0", + "RB:64-5_64-5_s0", + "RB:64-5_64-5_s0", + "RB:64-5_64-5_s0", + "RB:64-5_64-5_s0", + "C:16-3", + "C:4-3", + "C:1-3" + ], + [ + "RB:64-5_64-5_s1", + "RB:64-5_64-5_s0", + "RB:64-5_64-5_s0", + "RB:64-5_64-5_s0", + "RB:64-5_64-5_s0", + "RB:64-5_64-5_s0", + "C:16-3", + "C:4-3", + "C:1-3" + ], + [ + "RB:32-5_32-5_s1", + "RB:32-5_32-5_s0", + "RB:32-5_32-5_s0", + "RB:32-5_32-5_s0", + "RB:32-5_32-5_s0", + "RB:32-5_32-5_s0", + "C:8-3", + "C:1-3" + ], + [ + "RB:16-5_16-5_s1", + "RB:16-5_16-5_s0", + "RB:16-5_16-5_s0", + "RB:16-5_16-5_s0", + "RB:16-5_16-5_s0", + "RB:16-5_16-5_s0", + "C:4-3", + "C:1-3" + ], + [ + "RB:8-5_8-5_s1", + "RB:8-5_8-5_s0", + "RB:8-5_8-5_s0", + "RB:8-5_8-5_s0", + "RB:8-5_8-5_s0", + "RB:8-5_8-5_s0", + "C:1-3" + ] + ], + "down_conv_filters": null, + "down_conv_kernel_size": 4, + "down_mode": "STRIDED", + "encoder_resblocks": [ + [ + "RB:64-5_64-5_s1" + ], + [ + "RB:64-5_64-5_s1" + ], + [ + "RB:32-5_32-5_s1" + ], + [ + "RB:16-5_16-5_s1" + ], + [ + "RB:8-5_8-5_s1" + ] + ], + "input_blocks": [ + "RB:8-5_8-5_s1", + "RB:8-5_8-5_s0", + "RB:16-6_16-5_s2-2" + ], + "input_levels": 1, + "level_scale_factor": 2, + "normalization": "LAYER", + "num_levels": 5, + "output_activation": "none", + "output_blocks": [], + "output_channels": 1, + "output_conv_kernel_size": 0, + "output_mode": "RESIDUAL_WEIGHTED", + "share_decoder": false, + "share_down_layer": false, + "share_encoder": false, + "share_input_layer": true, + "share_output_layer": false, + "share_up_layer": false, + "skip_merge_mode": "CONCAT", + "up_conv_filters": [ + 64, + 64, + 32, + 16, + 8 + ], + "up_conv_kernel_size": 4, + "up_mode": "NNSAMPLE" + }, + "skip_merge_weight_schedule": { + "base": 2.0, + "max": 1.0, + "min": 1.0, + "offset": 250, + "scale": 1.0, + "schedule": [], + "start": 1.0, + "step": 0.0, + "type": "LINEAR" + }, + "start_level": 0, + "train_mode": "ALL", + "train_mode_schedule": { + "base": 2.0, + "max": Infinity, + "min": -Infinity, + "offset": 0, + "scale": 1.0, + "schedule": [], + "start": true, + "step": 0, + "type": "BOOLEAN" + } + }, + "light": { + "learning_rate": { + "base": 1, + "scale": 0, + "start": 0.001, + "type": "exponential" + }, + "max": 6.0, + "min": 0.01, + "optim_beta": 0.9, + "optimize": false + }, + "loss_active_eps": 1e-18, + "optimize_buoyancy": false, + "randomization": { + "grid_size_min": 3, + "grid_size_relative": 1, + "grow_mode": null, + "input_weights": null, + "inputs": [ + 2 + ], + "max_inputs": 5, + "max_targets": 1, + "min_inputs": 2, + "min_targets": 1, + "num_inputs_weights": null, + "num_targets_weights": null, + "scale_density_max": 1, + "scale_density_min": 1, + "scale_images_max": 1, + "scale_images_min": 1, + "sequence_length": true, + "target_weights": null, + "targets": [ + 2 + ], + "transform": false + }, + "resource_device": "/gpu:0", + "sequence_length": -1, + "summary_interval": 250, + "train_frame_encoders": true, + "train_res_down": 6, + "velocity": { + "CFL_loss": 0.2, + "CFL_loss_MS_weighting": { + "base": 2.0, + "max": Infinity, + "min": -Infinity, + "offset": 0, + "scale": 1.0, + "schedule": [], + "start": 1.0, + "step": 0, + "type": "CONST" + }, + "MS_coherence_loss": 0.0, + "MS_coherence_loss_MS_weighting": { + "base": 2.0, + "max": Infinity, + "min": -Infinity, + "offset": 0, + "scale": 1.0, + "schedule": [], + "start": 1.0, + "step": 0, + "type": "CONST" + }, + "cossim_loss": 0.0, + "decoder": { + "active": true, + "downscale_input_modes": [ + "RESAMPLE", + "RESAMPLE" + ], + "grow_intervals": [], + "input_type": "PREPROC", + "min_grid_res": 4, + "model": { + "alpha": 0.2, + "conv_activation": "relu", + "conv_padding": "ZERO", + "create_inputs": false, + "decoder_resblocks": [ + "RB:16-7_16-7_s0", + "RB:16-7_16-7_s0", + "RB:16-7_16-7_s0", + "RB:16-7_16-7_s0", + "RB:16-7_16-7_s0" + ], + "down_conv_filters": null, + "down_conv_kernel_size": 4, + "down_mode": "NONE", + "encoder_resblocks": [ + "RB:16-7_16-7_s0", + "RB:16-7_16-7_s0", + "RB:16-7_16-7_s0", + "RB:16-7_16-7_s0", + "RB:16-7_16-7_s0" + ], + "input_blocks": [ + "C:16-1" + ], + "input_levels": 1, + "level_scale_factor": 1.4, + "normalization": "LAYER", + "num_levels": 1, + "output_activation": "none", + "output_blocks": [ + "C:16-1" + ], + "output_conv_kernel_size": 1, + "output_mode": "SINGLE", + "share_decoder": false, + "share_down_layer": true, + "share_encoder": true, + "share_input_layer": true, + "share_output_layer": true, + "share_up_layer": true, + "skip_merge_mode": "CONCAT", + "up_conv_filters": 16, + "up_conv_kernel_size": 4, + "up_mode": "NNSAMPLE_CONV" + }, + "recursive_MS": false, + "recursive_MS_copy_on_grow": false, + "recursive_MS_direct_input": false, + "recursive_MS_levels": 1, + "recursive_MS_scale_factor": 2.0, + "recursive_MS_shared_model": true, + "recursive_MS_train_mode": "ALL", + "recursive_MS_use_max_level_input": false, + "share_downscale_encoder": false, + "skip_merge_weight_schedule": { + "base": 2.0, + "max": 1.0, + "min": 1.0, + "offset": 250, + "scale": 1.0, + "schedule": [], + "start": 1.0, + "step": 0.0, + "type": "LINEAR" + }, + "start_level": 0, + "step_input_density": [ + 0 + ], + "step_input_density_proxy": [], + "step_input_density_target": [], + "step_input_features": [ + 0, + 1 + ], + "train_mode": "ALL", + "train_mode_schedule": { + "base": 2.0, + "max": Infinity, + "min": -Infinity, + "offset": 0, + "scale": 1.0, + "schedule": [], + "start": true, + "step": 0, + "type": "BOOLEAN" + }, + "type_input_features": [ + "ENC3D" + ], + "velocity_format": "CURL_STAGGERED", + "warp_input_indices": [ + 0, + 1 + ] + }, + "density_proxy_warp_loss": 0.0, + "density_target_warp_loss": 0.0, + "density_target_warp_loss_MS_weighting": { + "base": 2.0, + "max": Infinity, + "min": -Infinity, + "offset": 0, + "scale": 1.0, + "schedule": [], + "start": 1.0, + "step": 0, + "type": "CONST" + }, + "density_warp_loss": 0.0, + "density_warp_loss_MS_weighting": { + "base": 2.0, + "max": Infinity, + "min": -Infinity, + "offset": 0, + "scale": 1.0, + "schedule": [], + "start": 1.0, + "step": 0, + "type": "CONST" + }, + "divergence_loss": 0, + "divergence_loss_MS_weighting": { + "base": 2.0, + "max": Infinity, + "min": -Infinity, + "offset": 0, + "scale": 1.0, + "schedule": [], + "start": 1.0, + "step": 0, + "type": "CONST" + }, + "divergence_normalize": 0.0, + "error_functions": { + "CFL_loss": "SE", + "MS_coherence_loss": "SE", + "density_proxy_warp_loss": "SE", + "density_target_warp_loss": "SE", + "density_warp_loss": "SE", + "divergence_loss": "SE", + "magnitude_loss": "SE", + "velocity_warp_loss": "SE", + "volume_target_loss": "SE" + }, + "grow": { + "factor": 2.0, + "intervals": [ + 8000, + 8000, + 8000 + ], + "scale_magnitude": true + }, + "learning_rate": { + "base": 1.0, + "max": 1.0, + "min": 0.0, + "offset": -5000, + "scale": 0.0002, + "schedule": [], + "start": 0.0002, + "step": 0, + "type": "ROOT_DECAY" + }, + "magnitude_loss": 0.0, + "noise_std": { + "base": 2.0, + "max": Infinity, + "min": -Infinity, + "offset": 0, + "scale": 1.0, + "schedule": [], + "start": 0.0, + "step": 0, + "type": "CONST" + }, + "optim_beta": 0.9, + "pre_opt": { + "CFL_loss": 0.0, + "MS_coherence_loss": 0.0, + "cossim_loss": 0.0, + "density_proxy_warp_loss": 0, + "density_target_warp_loss": 4.1000000000000003e-10, + "density_warp_loss": 4.1000000000000003e-10, + "divergence_loss": 2.58e-09, + "first": { + "CFL_loss": 0.0, + "MS_coherence_loss": 0.0, + "cossim_loss": 0.0, + "density_proxy_warp_loss": 0, + "density_target_warp_loss": 4.1000000000000003e-10, + "density_warp_loss": 1, + "divergence_loss": { + "base": 2.0, + "max": 2.0, + "min": 0.0, + "offset": 1000, + "scale": 1.0, + "schedule": [], + "start": 0.0, + "step": 0.0004, + "type": "LINEAR" + }, + "grow": { + "factor": 2, + "intervals": [ + 1200, + 1400, + 1600, + 1800 + ], + "scale_magnitude": false + }, + "iterations": 16000, + "learning_rate": { + "base": 0.5, + "max": 1e-05, + "min": -Infinity, + "offset": 0, + "scale": 0.0005, + "schedule": [], + "start": 1e-05, + "step": 0, + "type": "exponential" + }, + "magnitude_loss": { + "base": 0.5, + "max": Infinity, + "min": -Infinity, + "offset": 0, + "scale": 0.0005, + "schedule": [], + "start": 0.02, + "step": 0, + "type": "exponential" + }, + "regularization": 0.0001, + "smoothness_loss": 0.0, + "smoothness_neighbours": 3, + "velocity_warp_loss": 0.0, + "volume_target_loss": 0.0 + }, + "grow": { + "factor": 1.2, + "intervals": [], + "scale_magnitude": true + }, + "iterations": 1200, + "learning_rate": 0.02, + "magnitude_loss": 0.0, + "regularization": 0.0001, + "seq_init": "WARP", + "smoothness_loss": 0.0, + "smoothness_neighbours": 3, + "velocity_warp_loss": 0.0, + "volume_target_loss": 0.0 + }, + "pre_optimization": false, + "regularization": 1e-06, + "smoothness_loss": 0.0, + "smoothness_neighbours": 3, + "train_decoder": false, + "velocity_warp_loss": 0.0, + "volume_target_loss": 0.0, + "warp_clamp": "MC_SMOOTH", + "warp_gradients": { + "active": false, + "decay": { + "base": 2.0, + "max": Infinity, + "min": -Infinity, + "offset": 0, + "scale": 1.0, + "schedule": [], + "start": 0.9, + "step": 0, + "type": "CONST" + }, + "weight": 100.0 + }, + "warp_order": 2 + }, + "view_encoder": { + "encoder": [ + "IDENTITY" + ], + "grow_intervals": [], + "lifting": "NETWORK", + "merge": "NETWORK_SUMPROD", + "min_grid_res": 4, + "model": { + "alpha": 0.2, + "conv_activation": "relu", + "conv_padding": "ZERO", + "create_inputs": false, + "decoder_resblocks": [], + "down_conv_filters": 8, + "down_conv_kernel_size": 4, + "down_mode": "STRIDED", + "encoder_resblocks": [ + "RB:8-5_8-5_s1", + "RB:8-5_8-5_s0", + "RB:16-6_16-5_s2-2", + "RB:16-5_16-5_s0", + "RB:32-6_32-5_s2-2", + "RB:32-5_32-5_s0" + ], + "input_blocks": [], + "input_levels": 1, + "level_scale_factor": 2, + "normalization": "LAYER", + "num_levels": 1, + "output_activation": "none", + "output_blocks": [], + "output_channels": 32, + "output_conv_kernel_size": 1, + "output_mode": "SINGLE", + "share_decoder": false, + "share_down_layer": true, + "share_encoder": false, + "share_input_layer": true, + "share_output_layer": true, + "share_up_layer": true, + "skip_merge_mode": "CONCAT", + "up_conv_filters": 8, + "up_conv_kernel_size": 4, + "up_mode": "NNSAMPLE_CONV" + }, + "skip_merge_weight_schedule": { + "base": 2.0, + "max": 1.0, + "min": 1.0, + "offset": 250, + "scale": 1.0, + "schedule": [], + "start": 1.0, + "step": 0.0, + "type": "LINEAR" + }, + "start_level": 0, + "train_mode": "ALL", + "train_mode_schedule": { + "base": 2.0, + "max": Infinity, + "min": -Infinity, + "offset": 0, + "scale": 1.0, + "schedule": [], + "start": true, + "step": 0, + "type": "BOOLEAN" + } + }, + "volume_encoder": { + "active": false, + "grow_intervals": [], + "min_grid_res": 4, + "model": { + "alpha": 0.2, + "conv_activation": "relu", + "conv_padding": "ZERO", + "create_inputs": true, + "decoder_resblocks": [], + "down_conv_filters": null, + "down_conv_kernel_size": 4, + "down_mode": "STRIDED", + "encoder_resblocks": [ + "RB:32-7_32-7_s0", + "RB:32-7_32-7_s0", + "RB:32-7_32-7_s0", + "RB:32-7_32-7_s0", + "RB:32-7_32-7_s0", + "RB:32-7_32-7_s0" + ], + "input_blocks": [ + "C:32-1" + ], + "input_levels": 1, + "level_scale_factor": 2, + "normalization": "LAYER", + "num_levels": 1, + "output_activation": "none", + "output_blocks": [], + "output_channels": 32, + "output_conv_kernel_size": 0, + "output_mode": "SINGLE", + "share_decoder": false, + "share_down_layer": true, + "share_encoder": false, + "share_input_layer": false, + "share_output_layer": true, + "share_up_layer": true, + "skip_merge_mode": "CONCAT", + "up_conv_filters": 1, + "up_conv_kernel_size": 4, + "up_mode": "NNSAMPLE_CONV" + }, + "skip_merge_weight_schedule": { + "base": 2.0, + "max": 1.0, + "min": 1.0, + "offset": 250, + "scale": 1.0, + "schedule": [], + "start": 1.0, + "step": 0.0, + "type": "LINEAR" + }, + "start_level": 0, + "train_mode": "ALL", + "train_mode_schedule": { + "base": 2.0, + "max": Infinity, + "min": -Infinity, + "offset": 0, + "scale": 1.0, + "schedule": [], + "start": true, + "step": 0, + "type": "BOOLEAN" + } + } + }, + "validation": { + "batch_size": 2, + "cmp_scalarFlow": false, + "cmp_scalarFlow_render": false, + "cmp_vol_targets": false, + "input_view_mask": [ + 2 + ], + "output_interval": 250, + "render_MS": false, + "render_cycle": true, + "render_cycle_steps": 8, + "render_density": true, + "render_shadow": false, + "render_target": true, + "render_velocity": true, + "simulation": 80, + "start": 60, + "stats": true, + "step": 50, + "stop": 141, + "synth_data_eval_setup": "SPHERE", + "synth_data_seed": 1802168824, + "synth_data_shape_types": 1, + "warp_test": [], + "warp_test_render": true + } +} diff --git a/configs/setup_velocity.json b/configs/setup_velocity.json new file mode 100644 index 0000000..6dec198 --- /dev/null +++ b/configs/setup_velocity.json @@ -0,0 +1,1292 @@ +{ + "data": { + "MS_images": false, + "MS_volume": false, + "SDF": false, + "batch_size": [ + 2, + -4 + ], + "clip_grid": false, + "clip_grid_pad": 4, + "crop_grid": true, + "density": { + "density_type": "SF", + "hull_image_blur_std": 1.0, + "hull_smooth_blur_std": 0.0, + "hull_threshold": 0.04, + "hull_volume_blur_std": 0.5, + "inflow": { + "active": false, + "height": "MAX", + "hull_height": 4 + }, + "initial_value": "data/scalarFlow/sim_{sim:06d}/reconstruction/density_{frame:06d}.npz", + "max": 255.0, + "min": 0.0, + "render_targets": false, + "scalarFlow_reconstruction": "data/scalarFlow/sim_{sim:06d}/reconstruction/density_{frame:06d}.npz", + "scale": 1, + "synthetic_target_density_scale": 1.0, + "target": "data/scalarFlow/sim_{sim:06d}/input/cam/imgsUnproc_{frame:06d}.npz", + "target_cam_ids": "ALL", + "target_flip_y": false, + "target_preproc": "data/scalarFlow/sim_{sim:06d}/input/postprocessed/imgs_{frame:06d}.npz", + "target_scale": 1.5, + "target_threshold": 0.04, + "target_type": "PREPROC" + }, + "discriminator": { + "crop_size": [ + 96, + 96 + ], + "density_type": "SF", + "frames": [ + 35, + 125, + 1 + ], + "gamma_fake": [ + 0.5, + 2 + ], + "gamma_real": [ + 0.5, + 2 + ], + "initial_value": "data/scalarFlow/sim_{sim:06d}/reconstruction/density_{frame:06d}.npz", + "real_res_down": 4, + "render_targets": false, + "rotation_mode": "NONE", + "scale_fake": [ + 0.7, + 1.4 + ], + "scale_input_to_crop": false, + "scale_range": [ + 0.85, + 1.15 + ], + "scale_real": [ + 0.8, + 1.8 + ], + "scale_real_to_cam": true, + "simulations": [ + 0, + 20 + ], + "target": "data/scalarFlow/sim_{sim:06d}/input/cam/imgsUnproc_{frame:06d}.npz", + "target_preproc": "data/scalarFlow/sim_{sim:06d}/input/postprocessed/imgs_{frame:06d}.npz", + "target_type": "RAW" + }, + "grid_size": 64, + "hull": "TARGETS", + "initial_buoyancy": [ + 0.0, + 0.0, + 0.0 + ], + "load_sequence": null, + "load_sequence_pre_opt": false, + "rand_seed_global": 460585320, + "randomize": 64, + "res_down_factor": 640, + "resource_device": "/cpu:0", + "run_dirs": [ + "runs/train_velDens_sequence" + ], + "scalarFlow_frame_offset": -11, + "sequence_length": 5, + "sequence_step": [ + 1, + 2, + 3, + 4 + ], + "sims": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19 + ], + "simulation": 0, + "start": 20, + "step": 2, + "stop": 141, + "synth_shapes": { + "active": false, + "init_center": false, + "max_translation": 0.8, + "shape_types": [ + 0, + 1, + 2, + 5 + ] + }, + "velocity": { + "boundary": "CLAMP", + "init_mask": "HULL_TIGHT_NEXT", + "init_std": 0.1, + "initial_value": "data/scalarFlow/sim_{sim:06d}/reconstruction/velocity_{frame:06d}.npz", + "load_step": 1, + "scalarFlow_reconstruction": "data/scalarFlow/sim_{sim:06d}/reconstruction/velocity_{frame:06d}.npz" + }, + "y_scale": 1.5 + }, + "debug": { + "disc_dump_samples": false, + "print_weight_grad_stats": false, + "target_dump_samples": false + }, + "desc": "Sample setup for training of the velocity estimator with ScalarFlow data.", + "paths": { + "base": "runs", + "group": "train_velDens_sequence" + }, + "rendering": { + "SDF_threshold": 0.02, + "allow_fused_rendering": true, + "allow_static_cameras": false, + "background": { + "color": [ + 0, + 0.5, + 1.0 + ], + "type": "COLOR" + }, + "blend_mode": "BEER_LAMBERT", + "boundary": "BORDER", + "filter_mode": "LINEAR", + "lighting": { + "ambient_intensity": 0.64, + "initial_intensity": 0.85, + "shadow_resolution": [ + 128, + 96, + 96 + ] + }, + "luma": [ + 0.2126, + 0.7152, + 0.0722 + ], + "main_camera": { + "base_resolution": [ + 256, + 1920, + 1080 + ], + "distance": 0.8, + "far": 1.3, + "fov": 40, + "near": 0.3, + "resolution_scale": 0.3333333333333333 + }, + "mip": { + "bias": 0.0, + "level": 4, + "mode": "LINEAR" + }, + "monochrome": false, + "num_images": 24, + "sample_gradients": true, + "steps_per_cycle": 24, + "synthetic_target": { + "ambient_intensity": 0.64, + "blend_mode": "BEER_LAMBERT", + "filter_mode": "LINEAR", + "initial_intensity": 0.85 + }, + "target_cameras": { + "calibration_file": "scalaFlow_cameras.json", + "camera_ids": [ + 2, + 1, + 0, + 4, + 3 + ], + "crop_frustum": false, + "crop_frustum_pad": 2 + }, + "velocity_scale": 1024 + }, + "title": "velocity_ScalarFlow", + "training": { + "MS_weighting": null, + "allow_MS_losses": true, + "checkpoint_interval": 125, + "density": { + "SDF_pos_loss": 0, + "camera_jitter": false, + "center_loss": 0.001, + "decoder": { + "active": true, + "base_SDF_mode": "NONE", + "base_input": "ZERO", + "grow_intervals": [], + "input_type": "PREPROC", + "min_grid_res": 4, + "model": "[RUNID:000000-000000]density_decoder", + "recursive_MS": false, + "recursive_MS_copy_on_grow": false, + "recursive_MS_direct_input": false, + "recursive_MS_levels": 1, + "recursive_MS_residual": true, + "recursive_MS_scale_factor": 2.0, + "recursive_MS_shared_model": true, + "recursive_MS_train_mode": "ALL", + "skip_merge_weight_schedule": { + "base": 2.0, + "max": 1.0, + "min": 1.0, + "offset": 250, + "scale": 1.0, + "schedule": [], + "start": 1.0, + "step": 0.0, + "type": "LINEAR" + }, + "start_level": 0, + "step_input_density": [], + "step_input_density_target": [], + "step_input_features": [ + 0 + ], + "train_mode": "ALL", + "train_mode_schedule": { + "base": 2.0, + "max": Infinity, + "min": -Infinity, + "offset": 0, + "scale": 1.0, + "schedule": [], + "start": true, + "step": 0, + "type": "BOOLEAN" + }, + "type_input_features": [ + "ENC3D" + ], + "warp_input_indices": [ + 0 + ] + }, + "discriminator_loss": 2e-06, + "error_functions": { + "SDF_pos_loss": "AE", + "center_loss": "SE", + "hull": "SE", + "negative": "SE", + "preprocessed_target_loss": "SE", + "raw_target_loss": "SE", + "smoothness_loss": "SE", + "smoothness_loss_2": "SE", + "target_depth_smoothness_loss": "SE", + "temporal_smoothness_loss": "SE", + "volume_proxy_loss": "SE", + "volume_target_loss": "SE", + "warp_loss": "SE" + }, + "grow": { + "factor": 2.0, + "intervals": [], + "post_grow_actions": [], + "pre_grow_actions": [] + }, + "grow_lifting_lr": null, + "grow_lifting_residual": [ + { + "base": 2.0, + "max": Infinity, + "min": -Infinity, + "offset": 0, + "scale": 1.0, + "schedule": [], + "start": 0, + "step": 0, + "type": "CONST" + }, + { + "base": 2.0, + "max": Infinity, + "min": -Infinity, + "offset": 0, + "scale": 1.0, + "schedule": [], + "start": 0, + "step": 0, + "type": "CONST" + }, + { + "base": 2.0, + "max": Infinity, + "min": -Infinity, + "offset": 0, + "scale": 1.0, + "schedule": [], + "start": 0, + "step": 0, + "type": "CONST" + }, + { + "base": 2.0, + "max": Infinity, + "min": -Infinity, + "offset": 0, + "scale": 1.0, + "schedule": [], + "start": 0, + "step": 0, + "type": "CONST" + }, + { + "base": 2.0, + "max": Infinity, + "min": -Infinity, + "offset": 0, + "scale": 1.0, + "schedule": [], + "start": 0, + "step": 0, + "type": "CONST" + } + ], + "grow_lifting_skip": null, + "grow_lifting_train": null, + "grow_volenc_residual": null, + "hull": 0, + "learning_rate": { + "base": 1.0, + "max": 1, + "min": 0.0, + "offset": -5000, + "scale": 0.0002, + "schedule": [], + "start": 0.0004, + "step": 0, + "type": "ROOT_DECAY" + }, + "main_warp_fwd": true, + "negative": 0.0, + "optim_beta": 0.9, + "pre_opt": { + "SDF_pos_loss": 0.0, + "center_loss": 0.0, + "discriminator_loss": 0.0, + "first": { + "SDF_pos_loss": 0.0, + "center_loss": 0.0, + "discriminator_loss": 0.0, + "hull": 0.0, + "iterations": 0, + "learning_rate": { + "base": 0.5, + "max": Infinity, + "min": -Infinity, + "offset": 0, + "scale": 6.666666666666667e-05, + "schedule": [], + "start": 3.0, + "step": 0, + "type": "EXPONENTIAL" + }, + "negative": 0.0, + "preprocessed_target_loss": 0.0, + "raw_target_loss": 1.74e-05, + "regularization": 0.0001, + "smoothness_loss": 0.0, + "smoothness_loss_2": 0.0, + "smoothness_neighbours": 3, + "target_depth_smoothness_loss": 0.0, + "temporal_smoothness_loss": 0.0, + "volume_target_loss": 0.0, + "warp_loss": 0.0 + }, + "grow": { + "factor": 1.2, + "intervals": [] + }, + "hull": 0.0, + "inspect_gradients": 1, + "iterations": 2400, + "learning_rate": { + "base": 0.5, + "max": Infinity, + "min": -Infinity, + "offset": 0, + "scale": 0.0003333333333333333, + "schedule": [], + "start": 3.0, + "step": 0, + "type": "EXPONENTIAL" + }, + "negative": 0.0, + "preprocessed_target_loss": 0.0, + "raw_target_loss": 1.74e-05, + "regularization": 0.0001, + "seq_init": "WARP", + "smoothness_loss": 0.0, + "smoothness_loss_2": 0.0, + "smoothness_neighbours": 3, + "target_depth_smoothness_loss": 0.0, + "temporal_smoothness_loss": 0.0, + "volume_target_loss": 0.0, + "warp_loss": 0.0 + }, + "pre_optimization": false, + "preprocessed_target_loss": 0.0, + "raw_target_loss": 1, + "regularization": 1e-06, + "scale_render_grads_sharpness": 0.0, + "smoothness_loss": 0, + "smoothness_loss_2": 0, + "smoothness_neighbours": 1, + "target_depth_smoothness_loss": 0.0, + "temporal_smoothness_loss": 0.0, + "train_decoder": false, + "use_hull": true, + "view_interpolation": { + "steps": 0 + }, + "volume_proxy_loss": { + "base": 2.0, + "max": 0.01, + "min": 0.001, + "offset": 4000, + "scale": 1.0, + "schedule": [], + "start": 0.01, + "step": -1.125e-06, + "type": "LINEAR" + }, + "volume_target_loss": 0, + "warp_clamp": "MC_SMOOTH", + "warp_gradients": { + "active": true, + "decay": 0.9, + "update_first_only": true, + "weight": 1.0 + }, + "warp_loss": 0.0 + }, + "discriminator": { + "activation": "lrelu", + "activation_alpha": 0.2, + "active": true, + "cam_res_down": 6, + "conditional_hull": false, + "fake_camera_jitter": false, + "grow": { + "factor": 2.0, + "intervals": [] + }, + "history": { + "keep_chance": 0.01, + "load": null, + "reset_on_density_grow": true, + "samples": 4, + "save": false, + "sequence_reuse": true, + "size": 800 + }, + "kernel_size": 4, + "layers": [ + 16, + 16, + 24, + 24, + 32, + 32, + 32, + 64, + 64, + 64, + 16, + 4 + ], + "learning_rate": { + "base": 1.0, + "max": 1, + "min": 0.0, + "offset": -5000, + "scale": 0.0002, + "schedule": [], + "start": 0.0004, + "step": 0, + "type": "ROOT_DECAY" + }, + "loss_type": "RaLSGAN", + "model": "[RUNID:000000-000000]disc_model.h5", + "noise_std": 0.0, + "num_fake": 3, + "num_real": 4, + "optim_beta": 0.5, + "padding": "MIRROR", + "pre_opt": { + "first": { + "learning_rate": { + "base": 0.5, + "scale": 0.00013333333333333334, + "start": 0.0004, + "type": "exponential" + }, + "regularization": 0.002, + "train": false + }, + "learning_rate": 0.00016, + "regularization": 0.002, + "train": false + }, + "regularization": 0.002, + "start_delay": 0, + "steps": 1, + "stride": [ + 2, + 1, + 2, + 1, + 2, + 1, + 1, + 2, + 1, + 1, + 1, + 1 + ], + "target_label": 1.0, + "temporal_input": { + "active": false, + "step_range": [ + -3, + 4, + 1 + ] + }, + "train": true, + "use_fc": false + }, + "frame_merge_network": { + "active": false, + "grow_intervals": [], + "min_grid_res": 4, + "model": { + "alpha": 0.2, + "conv_activation": "relu", + "conv_padding": "ZERO", + "create_inputs": true, + "decoder_resblocks": [], + "down_conv_filters": null, + "down_conv_kernel_size": 4, + "down_mode": "STRIDED", + "encoder_resblocks": [ + "RB:64-5_64-5_s0", + "RB:48-5_48-5_s1", + "RB:32-5_32-5_s1" + ], + "input_blocks": [ + "C:64-1" + ], + "input_levels": 1, + "level_scale_factor": 2, + "normalization": "LAYER", + "num_levels": 1, + "output_activation": "none", + "output_blocks": [], + "output_conv_kernel_size": 0, + "output_mode": "SINGLE", + "share_decoder": false, + "share_down_layer": true, + "share_encoder": false, + "share_input_layer": false, + "share_output_layer": true, + "share_up_layer": true, + "skip_merge_mode": "CONCAT", + "up_conv_filters": 1, + "up_conv_kernel_size": 4, + "up_mode": "NNSAMPLE_CONV" + }, + "skip_merge_weight_schedule": { + "base": 2.0, + "max": 1.0, + "min": 1.0, + "offset": 250, + "scale": 1.0, + "schedule": [], + "start": 1.0, + "step": 0.0, + "type": "LINEAR" + }, + "start_level": 0, + "train_mode": "ALL", + "train_mode_schedule": { + "base": 2.0, + "max": Infinity, + "min": -Infinity, + "offset": 0, + "scale": 1.0, + "schedule": [], + "start": true, + "step": 0, + "type": "BOOLEAN" + } + }, + "frame_order": "BWD", + "iterations": 25000, + "lifting_network": { + "active": true, + "grow_intervals": [], + "min_grid_res": 4, + "model": "[RUNID:000000-000000]lifting_network", + "skip_merge_weight_schedule": { + "base": 2.0, + "max": 1.0, + "min": 1.0, + "offset": 250, + "scale": 1.0, + "schedule": [], + "start": 1.0, + "step": 0.0, + "type": "LINEAR" + }, + "start_level": 0, + "train_mode": "ALL", + "train_mode_schedule": { + "base": 2.0, + "max": Infinity, + "min": -Infinity, + "offset": 0, + "scale": 1.0, + "schedule": [], + "start": true, + "step": 0, + "type": "BOOLEAN" + } + }, + "light": { + "learning_rate": { + "base": 1, + "scale": 0, + "start": 0.001, + "type": "exponential" + }, + "max": 6.0, + "min": 0.01, + "optim_beta": 0.9, + "optimize": false + }, + "loss_active_eps": 1e-18, + "optimize_buoyancy": false, + "randomization": { + "grid_size_min": 3, + "grid_size_relative": 1, + "grow_mode": null, + "input_weights": null, + "inputs": [ + 2 + ], + "max_inputs": 5, + "max_targets": 1, + "min_inputs": 2, + "min_targets": 1, + "num_inputs_weights": null, + "num_targets_weights": null, + "scale_density_max": 1, + "scale_density_min": 1, + "scale_images_max": 1, + "scale_images_min": 1, + "sequence_length": false, + "target_weights": null, + "targets": [ + 2 + ], + "transform": false + }, + "resource_device": "/gpu:0", + "sequence_length": { + "base": 2.0, + "max": Infinity, + "min": -Infinity, + "offset": 0, + "scale": 1.0, + "schedule": [ + [ + 1500, + 2 + ], + [ + 1500, + 3 + ], + [ + 0, + -1 + ] + ], + "start": -1, + "step": 0, + "type": "SCHEDULE" + }, + "start_iteration": 0, + "summary_interval": 125, + "train_frame_encoders": false, + "train_res_down": 6, + "velocity": { + "CFL_loss": 0.1, + "CFL_loss_MS_weighting": { + "base": 2.0, + "max": Infinity, + "min": -Infinity, + "offset": 0, + "scale": 1.0, + "schedule": [], + "start": 1.0, + "step": 0, + "type": "CONST" + }, + "MS_coherence_loss": 0, + "MS_coherence_loss_MS_weighting": { + "base": 2.0, + "max": Infinity, + "min": -Infinity, + "offset": 0, + "scale": 1.0, + "schedule": [], + "start": 1.0, + "step": 0, + "type": "CONST" + }, + "cossim_loss": 0.0, + "decoder": { + "active": true, + "downscale_input_modes": [ + "RESAMPLE", + "RESAMPLE" + ], + "grow_intervals": [], + "input_type": "PREPROC", + "min_grid_res": 4, + "model": { + "alpha": 0.2, + "conv_activation": "relu", + "conv_padding": "ZERO", + "create_inputs": false, + "decoder_resblocks": [], + "down_conv_filters": null, + "down_conv_kernel_size": 4, + "down_mode": "NONE", + "encoder_resblocks": [ + "RB:32-5_32-5_s1", + "RB:32-5_32-5_s1", + "RB:16-5_16-5_s1", + "RB:8-5_8-5_s1", + "RB:4-5_4-5_s1" + ], + "input_blocks": [ + "C:16-1" + ], + "input_levels": 1, + "level_scale_factor": 1.4, + "normalization": "LAYER", + "num_levels": 1, + "output_activation": "none", + "output_blocks": [], + "output_conv_kernel_size": 1, + "output_mode": "SINGLE", + "share_decoder": false, + "share_down_layer": true, + "share_encoder": true, + "share_input_layer": true, + "share_output_layer": true, + "share_up_layer": true, + "skip_merge_mode": "CONCAT", + "up_conv_filters": 16, + "up_conv_kernel_size": 4, + "up_mode": "NNSAMPLE_CONV" + }, + "recursive_MS": true, + "recursive_MS_copy_on_grow": false, + "recursive_MS_direct_input": false, + "recursive_MS_levels": "VARIABLE", + "recursive_MS_residual_weight": [ + { + "base": 2.0, + "max": 1, + "min": 0, + "offset": 0, + "scale": 1.0, + "schedule": [], + "start": 0, + "step": 0.0006666666666666666, + "type": "LINEAR" + }, + { + "base": 2.0, + "max": 1, + "min": 0, + "offset": 500, + "scale": 1.0, + "schedule": [], + "start": 0, + "step": 0.0006666666666666666, + "type": "LINEAR" + }, + { + "base": 2.0, + "max": 1, + "min": 0, + "offset": 4500, + "scale": 1.0, + "schedule": [], + "start": 0, + "step": 0.0006666666666666666, + "type": "LINEAR" + }, + { + "base": 2.0, + "max": 1, + "min": 0, + "offset": 8500, + "scale": 1.0, + "schedule": [], + "start": 0, + "step": 0.0006666666666666666, + "type": "LINEAR" + }, + { + "base": 2.0, + "max": 1, + "min": 0, + "offset": 12500, + "scale": 1.0, + "schedule": [], + "start": 0, + "step": 0.0006666666666666666, + "type": "LINEAR" + } + ], + "recursive_MS_scale_factor": 2.0, + "recursive_MS_shared_model": true, + "recursive_MS_train_mode": "ALL", + "recursive_MS_use_max_level_input": false, + "share_downscale_encoder": false, + "skip_merge_weight_schedule": { + "base": 2.0, + "max": 1.0, + "min": 1.0, + "offset": 250, + "scale": 1.0, + "schedule": [], + "start": 1.0, + "step": 0.0, + "type": "LINEAR" + }, + "start_level": 0, + "step_input_density": [ + 0 + ], + "step_input_density_proxy": [ + 0, + 1 + ], + "step_input_density_target": [], + "step_input_features": [ + 0, + 1 + ], + "train_mode": "ALL", + "train_mode_schedule": { + "base": 2.0, + "max": Infinity, + "min": -Infinity, + "offset": 0, + "scale": 1.0, + "schedule": [], + "start": true, + "step": 0, + "type": "BOOLEAN" + }, + "type_input_features": [ + "INPUT_IMAGES_UNPROJECTION" + ], + "velocity_format": "CURL_STAGGERED", + "warp_input_indices": [ + 0, + 1, + 3 + ] + }, + "density_proxy_warp_loss": 0, + "density_target_warp_loss": 0.0, + "density_target_warp_loss_MS_weighting": { + "base": 2.0, + "max": Infinity, + "min": -Infinity, + "offset": 0, + "scale": 1.0, + "schedule": [], + "start": 1.0, + "step": 0, + "type": "CONST" + }, + "density_warp_loss": 0.0, + "density_warp_loss_MS_weighting": { + "base": 2.0, + "max": Infinity, + "min": -Infinity, + "offset": 0, + "scale": 1.0, + "schedule": [], + "start": 1.0, + "step": 0, + "type": "CONST" + }, + "divergence_loss": 0, + "divergence_loss_MS_weighting": { + "base": 2.0, + "max": Infinity, + "min": -Infinity, + "offset": 0, + "scale": 1.0, + "schedule": [], + "start": 1.0, + "step": 0, + "type": "CONST" + }, + "divergence_normalize": 0.0, + "error_functions": { + "CFL_loss": "SE", + "MS_coherence_loss": "SE", + "density_proxy_warp_loss": "SE", + "density_target_warp_loss": "SE", + "density_warp_loss": "SE", + "divergence_loss": "SE", + "magnitude_loss": "SE", + "velocity_warp_loss": "SE", + "volume_target_loss": "SE" + }, + "grow": { + "factor": 2.0, + "intervals": [], + "scale_magnitude": true + }, + "learning_rate": { + "base": 1.0, + "max": 1, + "min": 0.0, + "offset": -5000, + "scale": 0.0002, + "schedule": [], + "start": 0.0004, + "step": 0, + "type": "ROOT_DECAY" + }, + "magnitude_loss": 0, + "noise_std": { + "base": 2.0, + "max": Infinity, + "min": -Infinity, + "offset": 0, + "scale": 1.0, + "schedule": [], + "start": 0.0, + "step": 0, + "type": "CONST" + }, + "optim_beta": 0.9, + "pre_opt": { + "CFL_loss": 0.0, + "MS_coherence_loss": 0.0, + "cossim_loss": 0.0, + "density_proxy_warp_loss": 0, + "density_target_warp_loss": 4.1000000000000003e-10, + "density_warp_loss": 4.1000000000000003e-10, + "divergence_loss": 2.58e-09, + "first": { + "CFL_loss": 0.0, + "MS_coherence_loss": 0.0, + "cossim_loss": 0.0, + "density_proxy_warp_loss": 0, + "density_target_warp_loss": 4.1000000000000003e-10, + "density_warp_loss": 1, + "divergence_loss": { + "base": 2.0, + "max": 2.0, + "min": 0.0, + "offset": 1000, + "scale": 1.0, + "schedule": [], + "start": 0.0, + "step": 0.0004, + "type": "LINEAR" + }, + "grow": { + "factor": 2, + "intervals": [ + 1200, + 1400, + 1600, + 1800 + ], + "scale_magnitude": false + }, + "iterations": 16000, + "learning_rate": { + "base": 0.5, + "max": 1e-05, + "min": -Infinity, + "offset": 0, + "scale": 0.0005, + "schedule": [], + "start": 1e-05, + "step": 0, + "type": "exponential" + }, + "magnitude_loss": { + "base": 0.5, + "max": Infinity, + "min": -Infinity, + "offset": 0, + "scale": 0.0005, + "schedule": [], + "start": 0.02, + "step": 0, + "type": "exponential" + }, + "regularization": 0.0001, + "smoothness_loss": 0.0, + "smoothness_neighbours": 3, + "velocity_warp_loss": 0.0, + "volume_target_loss": 0.0 + }, + "grow": { + "factor": 1.2, + "intervals": [], + "scale_magnitude": true + }, + "iterations": 1200, + "learning_rate": 0.02, + "magnitude_loss": 0.0, + "regularization": 0.0001, + "seq_init": "WARP", + "smoothness_loss": 0.0, + "smoothness_neighbours": 3, + "velocity_warp_loss": 0.0, + "volume_target_loss": 0.0 + }, + "pre_optimization": false, + "regularization": 1e-06, + "smoothness_loss": 0.0001, + "smoothness_neighbours": 3, + "train_decoder": true, + "velocity_warp_loss": 0.0, + "volume_target_loss": 0.0, + "warp_clamp": "MC_SMOOTH", + "warp_gradients": { + "active": false, + "decay": { + "base": 2.0, + "max": Infinity, + "min": -Infinity, + "offset": 0, + "scale": 1.0, + "schedule": [], + "start": 0.9, + "step": 0, + "type": "CONST" + }, + "weight": 100.0 + }, + "warp_order": 2 + }, + "view_encoder": { + "encoder": [ + "IDENTITY" + ], + "grow_intervals": [], + "lifting": "NETWORK", + "merge": "NETWORK_SUMPROD", + "min_grid_res": 4, + "model": { + "alpha": 0.2, + "conv_activation": "relu", + "conv_padding": "ZERO", + "create_inputs": false, + "decoder_resblocks": [], + "down_conv_filters": 8, + "down_conv_kernel_size": 4, + "down_mode": "STRIDED", + "encoder_resblocks": [ + "RB:8-5_8-5_s1", + "RB:8-5_8-5_s0", + "RB:16-6_16-5_s2-2", + "RB:16-5_16-5_s0", + "RB:32-6_32-5_s2-2", + "RB:32-5_32-5_s0" + ], + "input_blocks": [], + "input_levels": 1, + "level_scale_factor": 2, + "normalization": "LAYER", + "num_levels": 1, + "output_activation": "none", + "output_blocks": [], + "output_channels": 32, + "output_conv_kernel_size": 1, + "output_mode": "SINGLE", + "share_decoder": false, + "share_down_layer": true, + "share_encoder": false, + "share_input_layer": true, + "share_output_layer": true, + "share_up_layer": true, + "skip_merge_mode": "CONCAT", + "up_conv_filters": 8, + "up_conv_kernel_size": 4, + "up_mode": "NNSAMPLE_CONV" + }, + "skip_merge_weight_schedule": { + "base": 2.0, + "max": 1.0, + "min": 1.0, + "offset": 250, + "scale": 1.0, + "schedule": [], + "start": 1.0, + "step": 0.0, + "type": "LINEAR" + }, + "start_level": 0, + "train_mode": "ALL", + "train_mode_schedule": { + "base": 2.0, + "max": Infinity, + "min": -Infinity, + "offset": 0, + "scale": 1.0, + "schedule": [], + "start": true, + "step": 0, + "type": "BOOLEAN" + } + }, + "volume_encoder": { + "active": false, + "grow_intervals": [], + "min_grid_res": 4, + "model": { + "alpha": 0.2, + "conv_activation": "relu", + "conv_padding": "ZERO", + "create_inputs": true, + "decoder_resblocks": [], + "down_conv_filters": null, + "down_conv_kernel_size": 4, + "down_mode": "STRIDED", + "encoder_resblocks": [ + "RB:32-7_32-7_s0", + "RB:32-7_32-7_s0", + "RB:32-7_32-7_s0", + "RB:32-7_32-7_s0", + "RB:32-7_32-7_s0", + "RB:32-7_32-7_s0" + ], + "input_blocks": [ + "C:32-1" + ], + "input_levels": 1, + "level_scale_factor": 2, + "normalization": "LAYER", + "num_levels": 1, + "output_activation": "none", + "output_blocks": [], + "output_channels": 32, + "output_conv_kernel_size": 0, + "output_mode": "SINGLE", + "share_decoder": false, + "share_down_layer": true, + "share_encoder": false, + "share_input_layer": false, + "share_output_layer": true, + "share_up_layer": true, + "skip_merge_mode": "CONCAT", + "up_conv_filters": 1, + "up_conv_kernel_size": 4, + "up_mode": "NNSAMPLE_CONV" + }, + "skip_merge_weight_schedule": { + "base": 2.0, + "max": 1.0, + "min": 1.0, + "offset": 250, + "scale": 1.0, + "schedule": [], + "start": 1.0, + "step": 0.0, + "type": "LINEAR" + }, + "start_level": 0, + "train_mode": "ALL", + "train_mode_schedule": { + "base": 2.0, + "max": Infinity, + "min": -Infinity, + "offset": 0, + "scale": 1.0, + "schedule": [], + "start": true, + "step": 0, + "type": "BOOLEAN" + } + } + }, + "validation": { + "batch_size": 2, + "cmp_scalarFlow": false, + "cmp_scalarFlow_render": false, + "cmp_vol_targets": false, + "input_view_mask": [ + 2 + ], + "output_interval": 125, + "render_MS": false, + "render_cycle": true, + "render_cycle_steps": 8, + "render_density": true, + "render_shadow": false, + "render_target": true, + "render_velocity": true, + "simulation": 80, + "start": 60, + "stats": true, + "step": 50, + "stop": 141, + "synth_data_eval_setup": "SPHERE", + "synth_data_seed": 1802168824, + "synth_data_shape_types": 5, + "warp_test": [], + "warp_test_render": true + } +} diff --git a/eval_runs.py b/eval_runs.py new file mode 100644 index 0000000..d685224 --- /dev/null +++ b/eval_runs.py @@ -0,0 +1,489 @@ +import subprocess, argparse, json, os, sys, copy +import munch +from lib.util import RunIndex, PartialFormatter + +SDC = 256 #step density correction +setup_warp_test = { + 'title':'eval{stats}{note}_gs{gridsize}_R{runid}_{title}', + 'desc':'evaluation of run {runid:} "{title}".', + 'paths':{ + 'group':"eval", + }, + 'rendering':{ + 'monochrome':False, + # 'luma':[], + 'filter_mode':'LINEAR', #NEAREST, LINEAR + #'boundary':'BORDER', #BORDER, CLAMP + 'mip':{ + 'mode':'LINEAR', #NONE, NEAREST, LINEAR + 'level':4, + 'bias':0.0, + }, + 'blend_mode':'BEER_LAMBERT', #BEER_LAMBERT, ALPHA, ADDITIVE + 'sample_gradients':True, + + 'steps_per_cycle':24, + 'num_images': 24, + + 'main_camera':{ + 'base_resolution':[256,1920,1080], #z(depth), y(height), x(width) + 'resolution_scale':1./3., # only for xy + 'fov':40, + 'near':0.3, + 'distance':0.8, + 'far':1.3, + }, + + 'allow_static_cameras':False, + 'allow_fused_rendering':True, + + 'target_cameras':{ + }, + + 'background':{ + 'type':'COLOR', #'CAM', 'COLOR', 'NONE'; only for vis-rendering, not used in optimization + 'color': [0,0.5,1.0], #[0,0.5,0], + }, + + 'lighting':{ + 'ambient_intensity':0.64, + 'initial_intensity':0.85,#2.4,#1.8, corrected up due to increased shadow falloff after step scaling fix + 'shadow_resolution':[256,196,196],#[64,64,64], #DHW + }, + "velocity_scale":2.0*SDC, + },#rendering + 'data':{ + 'run_dirs':['runs/sequence_reconstruction'], + 'grid_size':128, #resolution + + 'simulation':0, + 'start':82,#56,#40, + 'stop':90,#68,#52, #exclusive + 'step':2,#3, + "sims":[0], #,2,3,4], + 'scalarFlow_frame_offset': 0, #-11, + 'density':{ + 'scale': 1., #0.01*SDC, + 'initial_value':'data/scalarFlow/sim_{sim:06d}/reconstruction/density_{frame:06d}.npz', + 'min':0.0, + 'max':1.0 *SDC, + 'target_type': "RAW", #RAW, PREPROC, SYNTHETIC + 'target_cam_ids': "ALL", + 'target': 'data/scalarFlow/sim_{sim:06d}/input/cam/imgsUnproc_{frame:06d}.npz', + 'target_preproc': 'data/scalarFlow/sim_{sim:06d}/input/postprocessed/imgs_{frame:06d}.npz', + 'target_threshold':4e-2, + 'target_scale': 1.5, + 'hull_image_blur_std':1.0, + 'hull_volume_blur_std':0.5, + 'hull_smooth_blur_std':0.0,#2.5, + 'hull_threshold':4e-2, + 'inflow':{ + 'active':True, + 'hull_height':4, + 'height':'MAX', + }, + 'scalarFlow_reconstruction':'data/scalarFlow/sim_{sim:06d}/reconstruction/density_{frame:06d}.npz', + }, + 'velocity':{ + 'initial_value':'data/scalarFlow/sim_{sim:06d}/reconstruction/velocity_{frame:06d}.npz', + 'load_step':2, + 'init_std':0.1, + 'boundary':'CLAMP', #BORDER (closed 0 bounds), CLAMP (open bounds) + 'scalarFlow_reconstruction':'data/scalarFlow/sim_{sim:06d}/reconstruction/velocity_{frame:06d}.npz', + }, + 'initial_buoyancy':[0.,0.,0.], + 'discriminator':{ + 'simulations':[0,6], + 'frames':[45,145, 1], + 'target_type': "PREPROC", #RAW, PREPROC + #augmentation + 'crop_size':[96,96], + 'real_res_down': 4, + 'scale_range':[0.52,1.0], + 'scale_real':[0.8, 1.8], #range for random intensity scale on real samples, due to background substraction these tend to be darker than the images generated when using raw targets, so ~ *1.5 to correct + 'scale_fake':[0.7, 1.4], + 'gamma_real':[0.5,2], #range for random gamma correction on real samples (value here is inverse gamma) + 'gamma_fake':[0.5,2], #range for random gamma correction on fake samples (value here is inverse gamma) + + }, + 'load_sequence':'[RUNID:{runid:}]', #None, #only for rendering without optimization + 'load_sequence_pre_opt':False, + },#data + 'training':{ + 'iterations':0, + + 'resource_device':'/cpu:0', #'/cpu:0', '/gpu:0' + + #'loss':'L2', + 'train_res_down':6, + 'loss_active_eps':1e-08, #minimum absolute value of a loss scale for the loss to be considered active (to prevent losses scaled to (almost) 0 from being evaluated) + "view_encoder":{ + #"active":True, active= density.decoder.active or velocity.decoder.active + "model": "[RUNID:{runid:}]target_encoder{suffix}", + }, + "volume_encoder":{ + "active":False, + #"model":"decoder_model", + "model": "[RUNID:{runid:}]volume_encoder{suffix}", + }, + "lifting_network":{ + "active":False, + #"model":"decoder_model", + "model": "[RUNID:{runid:}]lifting_network{suffix}", + }, + "frame_merge_network":{ + "active":False, + #"model":"decoder_model", + "model": "[RUNID:{runid:}]frame_merge_network{suffix}", + }, + 'density':{ + 'use_hull':False, #[], + 'warp_clamp':"MC_SMOOTH", + 'pre_optimization':True, #whether pre-optim will run for density, affects forward propagation/advection of state and optimization + # to only have fwd advection without optimization set iterations to 0 + # to have pre-opt-like optimization without advection disable pre-opt and use loss schedules + "decoder":{ + "active":False, + "model":"[RUNID:{runid:}]density_decoder{suffix}", #path to model file + }, + 'pre_opt':{ + 'first':{ #settings for first frame + 'iterations':0, + }, + #settings for remaining frames + 'iterations':0, + 'inspect_gradients':False, + }, + 'grow':{ # not yet used + "factor":1.2,#2. factor per interval, max down-scaling is factor^len(intervals) + #iterations for each grow step, empty to disable + 'intervals':[], + }, + }, + 'velocity':{ + 'warp_order':2, + 'warp_clamp':"MC_SMOOTH", + # 'use_hull':False, + 'pre_optimization':False, + "decoder":{ + "active":True, + "model":"[RUNID:{runid:}]velocity_decoder{suffix}", #path to model file + "min_grid_res": 10, + "step_input_density": [0], #0 is the current frame, 1 the next, etc. can be negative + "step_input_density_target": [], #0 is the current frame, 1 the next, etc. can be negative + "step_input_density_proxy": [], #0 is the current frame, 1 the next, etc. can be negative + "step_input_features": [0,1], + "warp_input_indices": [0,1], #indices of network inputs to be warped. first the the density inputs, then the elements of step_input_features + "recursive_MS_levels": "VARIABLE", + "recursive_MS_scale_factor": 1.4, + }, + 'pre_opt':{ + 'first':{ #settings for first frame + 'iterations':0, + 'grow':{ + "factor":1.2, + "scale_magnitude":False, + 'intervals':[], + }, + + }, + #settings for remaining frames + 'iterations':0, + }, + + 'grow':{ + "factor":1.2,#2. factor per interval, max down-scaling is factor^len(intervals) + "scale_magnitude":False, + 'intervals':[], + }, + }, + 'optimize_buoyancy':False, + "light":{ + "optimize":False, + "min":0.01, + "max":6.0, + "learning_rate":{'type':'exponential', 'start':0.001, 'base':0.5, 'scale':2/3000},#0.0002, + }, + + 'discriminator':{ + 'active':False, + },#discriminator + 'summary_interval':100, + #'test_interval':250, + },#training + 'validation':{ + 'output_interval':100, + 'input_view_mask': None, #[0,1,2,3,4], + 'stats':False, + 'cmp_vol_targets':False, + 'cmp_scalarFlow':False, + 'cmp_scalarFlow_render':False, + 'warp_test':[],#False, + 'warp_test_render':False, + 'render_cycle':False, + 'render_target':False, + 'render_MS':False, + } +} + +def target_scale_from_grid_size(grid_size): + # down-scale factor for target/input images: 6 at 128, 12 at 64 + return int(6 * 128/grid_size) + +if __name__=='__main__': + base_dirs = ['./runs/test-and-debug/'] #'./runs/', './runs/test-and-debug/', '/data/erik/voldifrender/runs/sequence_recon_test/' + + parser = argparse.ArgumentParser(description='Load and evaluate or render runs.') + parser.add_argument('runIDs', metavar='R', type=str, nargs='*', default=[], help='ids/timestamps of the runs to test') + parser.add_argument('-d', '--baseDirs', dest='base_dirs', metavar='D', type=str, nargs='*', default=base_dirs, help='directories to search for runs') + parser.add_argument('-o', '--out', dest='out_path', type=str, default='/data/erik/voldifrender/runs_eval', help='path to result directory') + parser.add_argument('--noRecursive', dest='recursive', action='store_false', help='search base directories recursive') + parser.add_argument('-w','--warp', dest='warp', type=int, nargs='*', default=None, help='run warp test') + parser.add_argument('-W','--warpRender', dest='warp_render', nargs='*', default=None, help='run warp test with render, overrides -w') + parser.add_argument('--warpOrder', dest='warp_order', type=int, default=0, help='order of integration scheme used in the warp test, requires -w or -W') + parser.add_argument('-s','--scalarFlow', dest='sF', action='store_true', help='run scalarFlow comparison') + parser.add_argument('-S','--scalarFlowRender', dest='sF_render', action='store_true', help='run scalarFlow comparison with render, overrides -s') + parser.add_argument('-v','--volTar', dest='volTar', action='store_true', help='run volume target comparison. requires --stats') + #parser.add_argument('-V','--scalarFlowRender', dest='sF_render', action='store_true', help='run scalarFlow comparison with render, overrides -s') + parser.add_argument('-r','--render', dest='render', action='store_true', help='render sequence') + parser.add_argument('-g','--gridSize', dest='grid_size', type=int, default=64, help='resolution of grid') + parser.add_argument('--simulation', dest='simulation', type=int, default=0, help='id of SF sim to use.') + parser.add_argument('--frameRange', dest='frame_range', type=int, nargs=3, default=None, help='range (start, stop, step) of frames to use.') + parser.add_argument('--camIDs', dest='cam_ids', type=int, nargs='+', default=None, help='targets/views to use as inputs. defaults to all.') + #parser.add_argument('--viewMask', dest='view_mask', type=int, nargs='+', default=None, help='targets/views to use as targets. defaults to all.') + parser.add_argument('--stats', dest='stats', action='store_true', help='compute statistics.') + #parser.add_argument('--renderCycle', dest='render_cycle', action='store_true', help='render still cycle views, requires -r') + parser.add_argument('--renderCycle', dest='render_cycle', type=int, nargs='?', const=12, default=None, help='render still cycle views, requires -r') + parser.add_argument('--renderDensity', dest='render_density', action='store_true', help='render density, requires -r') + parser.add_argument('--renderShadow', dest='render_shadow', action='store_true', help='render render high density with shadow, requires --renderDensity') + parser.add_argument('--renderTarget', dest='render_target', action='store_true', help='render target views, requires -r') + parser.add_argument('--renderVelocity', dest='render_velocity', action='store_true', help='render velocity, requires -r') + parser.add_argument('--renderMS', dest='render_MS', action='store_true', help='render multi-scale grids, if available. requires -r') + parser.add_argument('--testCams', dest='test_cams', action='store_true', help='render test cams, requires --renderTarget') + parser.add_argument('--preOpt', dest='pre_opt', action='store_true', help='load pre-optimization result') + parser.add_argument('--densScale', dest='dens_scale', type=float, default=1, help='scaling factor for density') + parser.add_argument('--lightIntensity', dest='light_intensity', type=float, default=None, help='intensity for the shadow point light') + parser.add_argument('--ambientIntensity', dest='ambient_intensity', type=float, default=None, help='intensity for the ambient light') + parser.add_argument('--bkgColor', dest='bkg_color', type=float, nargs=3, default=None, help='RGB background color') + parser.add_argument('-t', '--title', dest='title', type=str, default=None, help='title, overrides original run title') + parser.add_argument('-n', '--note', dest='note', type=str, default=None, help='prepends a note to the title') + parser.add_argument('--device', dest='device', type=str, default=None, help='GPU to use') + parser.add_argument('--debug', dest='debug', action='store_true', help='run eval script in debug mode') + parser.add_argument('--modelName', dest='modelName', type=str, default='', help='model name suffix to use. e.g.: "", "_ckp", "_part". Defaults to the final model "".') + parser.add_argument('--evalDataSetup', dest='evalDataSetup', type=str, default='SPHERE', help='data-setup for evaluation: SF, SPHERE, CUBE, ROTCUBE.') + parser.add_argument('--evalDataRunID', dest='evalDataRunID', type=str, default=None, help='. requires --evalDataSetup SF.') + parser.add_argument('--synthMaxT', dest='synthMaxT', type=float, default=None, help='maximum translation for SPHERE, CUBE.') + parser.add_argument('--saveVol', dest='save_volume', action='store_true', help='save final volumes.') + + args = parser.parse_args() + + setup_file = os.path.join(args.out_path, "warp_test_setup.json") + + run_index = RunIndex(args.base_dirs, recursive=args.recursive) + if not args.runIDs: + runs = run_index.query_runs() + else: + runs = [run_index.runs[_] for _ in args.runIDs] + + f = PartialFormatter() + + os.makedirs(args.out_path, exist_ok=True) + c = r'-\|/' + interval = 0.5 + i=0 + for run_entry in runs: + runid = run_entry.runid + run_config = munch.munchify(run_entry.config) + setup = munch.munchify(copy.deepcopy(setup_warp_test)) + stats_title = "" + + setup.paths.base = args.out_path + setup.paths.group = "" + setup.data.run_dirs += [run_entry.parentdir] + setup.data.load_sequence = f.format(setup.data.load_sequence, runid=runid) + if args.pre_opt: + stats_title += '_pO' + setup.data.load_sequence_pre_opt = args.pre_opt + if args.stats: + stats_title += '_stats' + setup.validation.stats = True + if args.sF_render: + stats_title += '_sFcmpR' + setup.validation.cmp_scalarFlow = True + setup.validation.cmp_scalarFlow_render = True + elif args.sF: + stats_title += '_sFcmp' + setup.validation.cmp_scalarFlow = True + if args.volTar: + stats_title += '_vTcmp' + setup.validation.cmp_vol_targets = True + warp_args = args.warp + warp_title = '_warp' + if args.warp_render is not None: + warp_args = args.warp_render + warp_title = '_warpR' + setup.validation.warp_test_render = True + if warp_args is not None: + stats_title += warp_title + setup.validation.warp_test = warp_args if len(warp_args)>0 else ["BASE"] #, "FIXED_INFLOW" + if args.warp_order>0: + setup.training.velocity.warp_order = args.warp_order + stats_title += '-%d' % args.warp_order + else: + setup.training.velocity.warp_order = run_config.training.velocity.warp_order + if "warp_clamp" in run_config.training.density: + setup.training.density.warp_clamp = run_config.training.density.warp_clamp + if "warp_clamp" in run_config.training.velocity: + setup.training.velocity.warp_clamp = run_config.training.velocity.warp_clamp + + if args.light_intensity is not None: + setup.rendering.lighting.initial_intensity = args.light_intensity + elif run_entry.scalars.get("light_intensity") is not None: + setup.rendering.lighting.initial_intensity = run_entry.scalars["light_intensity"][0] + else: + setup.rendering.lighting.initial_intensity = run_config.rendering.lighting.initial_intensity + if args.ambient_intensity is not None: + setup.rendering.lighting.ambient_intensity = args.ambient_intensity + elif run_entry.scalars.get("light_intensity") is not None: + setup.rendering.lighting.ambient_intensity = run_entry.scalars["light_intensity"][1] + else: + setup.rendering.lighting.ambient_intensity = run_config.rendering.lighting.ambient_intensity + if args.test_cams: + setup.rendering.target_cameras.calibration_file = "test_cameras.json" + setup.rendering.target_cameras.camera_ids = [_ for _ in range(32)] + elif "target_cameras" in run_config.rendering: + setup.rendering.target_cameras = run_config.rendering.target_cameras + if args.bkg_color is not None: + setup.rendering.background.color = args.bkg_color + + setup.rendering.boundary = run_config.rendering.get("boundary", None) + #test override + #setup.training.density.warp_clamp = "MC_SMOOTH" + #setup.training.velocity.warp_clamp = "MC_SMOOTH" + + setup.training.view_encoder.lifting = run_config.training.view_encoder.lifting + setup.training.view_encoder.encoder = run_config.training.view_encoder.encoder + if "NETWORK" in run_config.training.view_encoder.encoder: + setup.training.view_encoder.model = f.format(setup.training.view_encoder.model, runid=runid, suffix=args.modelName) + + if run_config.training.volume_encoder.active: + setup.training.volume_encoder.active = True + setup.training.volume_encoder.model = f.format(setup.training.volume_encoder.model, runid=runid, suffix=args.modelName) + + if "lifting_network" in run_config.training and run_config.training.lifting_network.active: + setup.training.lifting_network.active = True + setup.training.lifting_network.model = f.format(setup.training.lifting_network.model, runid=runid, suffix=args.modelName) + + if run_config.training.frame_merge_network.active: + setup.training.frame_merge_network.active = True + setup.training.frame_merge_network.model = f.format(setup.training.frame_merge_network.model, runid=runid, suffix=args.modelName) + + if run_config.training.density.decoder.active: + setup.training.density.decoder.model = f.format(setup.training.density.decoder.model, runid=runid, suffix=args.modelName) + setup.training.density.decoder.recursive_MS = run_config.training.density.decoder.recursive_MS + setup.training.density.decoder.recursive_MS_scale_factor = run_config.training.density.decoder.model.level_scale_factor if not hasattr(run_config.training.density.decoder, "recursive_MS_scale_factor") else run_config.training.density.decoder.recursive_MS_scale_factor + setup.training.density.decoder.recursive_MS_direct_input = run_config.training.density.decoder.get("recursive_MS_direct_input", False) + setup.training.density.decoder.recursive_MS_levels = "VARIABLE" #1 #run_config.training.density.decoder.recursive_MS_levels # + setup.training.density.decoder.recursive_MS_residual = run_config.training.density.decoder.get("recursive_MS_residual", True) + setup.training.density.decoder.min_grid_res = run_config.training.density.decoder.min_grid_res #5 # + setup.training.density.decoder.base_input = run_config.training.density.decoder.get("base_input", "ZERO") + setup.training.density.decoder.step_input_density = run_config.training.density.decoder.step_input_density + setup.training.density.decoder.step_input_density_target = run_config.training.density.decoder.step_input_density_target + setup.training.density.decoder.step_input_features = run_config.training.density.decoder.step_input_features + #setup.training.density.decoder.warp_input_indices = run_config.training.density.decoder.warp_input_indices + else: + setup.training.density.decoder.model = None + + #print(run_config.training.velocity.decoder.active) + setup.training.velocity.decoder.model = f.format(setup.training.velocity.decoder.model, runid=runid, suffix=args.modelName) if run_config.training.velocity.decoder.active else None + setup.training.velocity.decoder.recursive_MS = run_config.training.velocity.decoder.recursive_MS + setup.training.velocity.decoder.recursive_MS_scale_factor = run_config.training.velocity.decoder.model.level_scale_factor if not hasattr(run_config.training.velocity.decoder, "recursive_MS_scale_factor") else run_config.training.velocity.decoder.recursive_MS_scale_factor + setup.training.velocity.decoder.recursive_MS_levels = "VARIABLE" #1 #run_config.training.velocity.decoder.recursive_MS_levels # + setup.training.velocity.decoder.min_grid_res = run_config.training.velocity.decoder.min_grid_res #5 # + setup.training.velocity.decoder.step_input_density = run_config.training.velocity.decoder.step_input_density + setup.training.velocity.decoder.step_input_density_target = run_config.training.velocity.decoder.step_input_density_target + setup.training.velocity.decoder.step_input_density_proxy = run_config.training.velocity.decoder.get("step_input_density_proxy", []) + setup.training.velocity.decoder.step_input_features = run_config.training.velocity.decoder.step_input_features + setup.training.velocity.decoder.warp_input_indices = run_config.training.velocity.decoder.warp_input_indices + setup.training.velocity.decoder.velocity_format = run_config.training.velocity.decoder.velocity_format + + #setup.training.view_encoder.load_encoder = f.format(setup.training.view_encoder.load_encoder, runid=runid) + + setup.desc = f.format(setup.desc, runid=runid, title=run_entry.title) + + setup.data.res_down_factor = run_config.data.res_down_factor + setup.data.grid_size = args.grid_size + setup.data.y_scale = run_config.data.y_scale + #setup.data.y_scale = 1.5 + setup.training.train_res_down = target_scale_from_grid_size(args.grid_size) + setup.data.simulation = args.simulation #run_config.data.simulation + setup.data.sims = [setup.data.simulation] + if args.frame_range is not None: + setup.data.start, setup.data.stop, setup.data.step = args.frame_range + else: + setup.data.start = run_config.data.start + setup.data.stop = run_config.data.stop + setup.data.step = run_config.data.step + setup.data.velocity.load_step = run_config.data.step + #setup.data.density.initial_value = f.format(setup.data.density.initial_value, runid=runid) + setup.data.density.scale = args.dens_scale + #setup.data.velocity.initial_value = f.format(setup.data.velocity.initial_value, runid=runid) + if args.synthMaxT is not None: + setup.data.synth_shapes = munch.Munch(max_translation = args.synthMaxT) + + #setup.validation.input_view_mask = args.view_mask if args.view_mask is not None else run_config.validation.input_view_mask + setup.validation.render_cycle = args.render_cycle is not None + setup.validation.render_cycle_steps = args.render_cycle if args.render_cycle is not None else 12 + setup.validation.render_density = args.render_density + setup.validation.render_shadow = args.render_shadow + setup.validation.render_target = args.render_target + setup.validation.render_velocity = args.render_velocity + setup.validation.render_MS = args.render_MS + + setup.validation.synth_data_eval_setup = args.evalDataSetup.upper() + if args.evalDataSetup.upper()=="SF": + if args.evalDataRunID is not None: + setup.data.density.initial_value = f.format('[RUNID:{runid:}]frame_{frame:06d}/density.npz', runid=args.evalDataRunID) + setup.data.velocity.initial_value = f.format('[RUNID:{runid:}]frame_{frame:06d}/velocity.npz', runid=args.evalDataRunID) + setup.data.scalarFlow_frame_offset = 0 + else: + setup.data.density.initial_value = run_config.data.density.initial_value + setup.data.density.target = run_config.data.density.target + setup.data.density.target_preproc = run_config.data.density.target_preproc + setup.data.velocity.initial_value = run_config.data.velocity.initial_value + setup.data.scalarFlow_frame_offset = run_config.data.scalarFlow_frame_offset + + + setup.data.SDF = run_config.data.SDF + + if args.cam_ids is not None: + setup.data.density.target_cam_ids = args.cam_ids + #setup.validation.input_view_mask = args.cam_ids + stats_title = "_c%s"%(len(args.cam_ids),) + stats_title + if isinstance(run_config.training.randomization.inputs, list): + setup.validation.input_view_mask = run_config.training.randomization.inputs + + setup.title = f.format(setup.title, note=("_"+args.note if args.note is not None else ""), stats=stats_title, gridsize=args.grid_size, runid=runid, title=(run_entry.title if args.title is None else args.title)) + # setup.training.velocity.pre_optimization = args.warp_vel + #if "target_cam_ids" in run_config.data.density: + # setup.data.density.target_cam_ids = run_config.data.density.target_cam_ids + with open(setup_file, 'w') as file: + json.dump(setup, file) + file.flush() + cmd = ['python', "reconstruct_sequence.py", + '--setup', setup_file, + # '--fit', #run optimization + # '--noRender', + # '--noConsole', + ] + if not args.render: + cmd.append('--noRender') + if args.device is not None: + cmd.append('--device') + cmd.append(args.device) + if args.debug: + cmd.append("--debug") + if args.save_volume: + cmd.append("--saveVol") + p = subprocess.Popen(cmd) + p.communicate() diff --git a/images/Framework.PNG b/images/Framework.PNG new file mode 100644 index 0000000000000000000000000000000000000000..d9d9fd0bf7503df078a9a427c437221a4ea51a0b GIT binary patch literal 200269 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%0D&eodw1_k@l8 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

9Ce}; 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�_|an{g^$C_0_*oQS?@OZj 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 U72E6CM%?!oaxZ1erA>qWA2$@YNB{r; literal 0 HcmV?d00001 diff --git a/images/SF_targets.PNG b/images/SF_targets.PNG new file mode 100644 index 0000000000000000000000000000000000000000..37cff5fe9661d929d0c988066a6517b015774548 GIT binary patch literal 442991 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

W!dYzqkgE$2RHsCD(iJ$qd;kO%TzwcD)S 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%0As}@&&a_ImGIF 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

N97kqfdih@{g~;BBS6%h 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 z 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 WeEQz|$9@6+JAKmHqWJjLJO2xWn3b6T literal 0 HcmV?d00001 diff --git a/phitest/render/__init__.py b/phitest/render/__init__.py new file mode 100644 index 0000000..69bf833 --- /dev/null +++ b/phitest/render/__init__.py @@ -0,0 +1,12 @@ + +from .camera import Camera +from .lighting import Light, PointLight, SpotLight +from .renderer import Renderer, RenderingContext +from .vector import GridShape, Vector2, Int2, Float2, Vector3, Int3, Float3, Vector4, Int4, Float4 +from .transform import MatrixTransform, Transform, GridTransform +from .data_structures import DensityGrid, VelocityGrid, State, Sequence, Zeroset, ImageSet, ImageSetMS +from .neural_data_structures import NeuralVelocityGrid, NeuralDensityGrid, NeuralState +from .optimization import OptimizationContext, DiscriminatorContext, LossSchedules +from .optimization import loss_dens_target, loss_dens_target_raw, loss_dens_smooth, loss_dens_warp, loss_dens_disc, loss_vel_warp_dens, loss_vel_warp_vel, loss_vel_smooth, loss_vel_divergence, loss_disc_real, loss_disc_fake +from .optimization import optStep_density, optStep_velocity, optStep_state, optStep_sequence, optStep_discriminator +from .generator_models import get_view_encoder, get_density_decoder, GrowingUNet, LiftingNetwork, ScalarMul, WeightedSum, SDFDiffAENetwork, SDFDiffRefinerNetwork, RWDensityGeneratorNetwork, RWVelocityGeneratorNetwork \ No newline at end of file diff --git a/phitest/render/camera.py b/phitest/render/camera.py new file mode 100644 index 0000000..d6b6a6f --- /dev/null +++ b/phitest/render/camera.py @@ -0,0 +1,464 @@ +import math +import numpy as np +from .serialization import to_dict, from_dict +from .transform import MatrixTransform, GridTransform +from .vector import Float2, Float3, Float4 +import logging + +LOG = logging.getLogger("Camera") + +class LuTCache(object): + def __init__(self, object_grid_transform, camera): + self.obj = object_grid_transform + self.cam = camera + self.LuT = None + self.inverseLuT = None + + def check_lut(self, object_grid_transform, camera): + return (object_grid_transform==self.obj) and (camera==self.cam) + + def get_size(): + pass + + +class Camera(object): + # fov: horizontal degree + # aspect: width/height + # jitter: scalar or array-like with shape (3,) + # static: precompute LuT and LoD w.r.t this grid transform + def __init__(self, grid_transform, nearFar=[1,10], topRightBottomLeft=[1,1,-1,-1], fov=None, aspect=1, perspective=True, static=None, jitter=None): + assert isinstance(grid_transform, MatrixTransform) + self.transform=grid_transform + self.jitter = jitter + self.trbl = topRightBottomLeft + self.clip = nearFar + self.perspective = perspective + if fov is not None: + self.set_fov(fov, aspect) + self.static = static + self.LuT = None + self.inverseLuT = None + self.scissor_pad = None + #@classmethod + #def from_eye_lookat(cls, eye, lookat, up, resolution, depth, topRightBottomLeft=[1,1,-1,-1], fov=None, aspect=1, perspective=True): + # + @classmethod + def from_dict(cls, d): + t = d.pop("grid_transform") + t = from_dict(t) + return cls(grid_transform=t,**d) + + def view_transform_inverse(self, with_jitter=True): + if self.jitter is not None and with_jitter: + jitter_position = np.random.uniform(-self.jitter,self.jitter, [3]).astype(np.float32) + #LOG.debug("Camera jitter position: %s", jitter_position) + view_transform = GridTransform(self.transform.grid_size, translation=jitter_position, parent=self.transform) + else: + view_transform = self.transform + return view_transform + def view_matrix_inverse(self, with_jitter=True): + return self.view_transform_inverse(with_jitter).get_transform_matrix() + def view_matrix(self, with_jitter=True): + return self.view_transform_inverse(with_jitter).get_inverse_transform() + + def frustum(self): + return np.asarray([self.clip[0], self.clip[1], self.trbl[3], self.trbl[1], self.trbl[0], self.trbl[2]], dtype=np.float32) + #aspect: width/height if not vertical, else height/width + def set_fov(self, fov, aspect=1, vertical=False): + half_h = np.tan(fov/2. * np.pi/180.)*self.clip[0] + half_v = half_h/aspect + if vertical: + self.trbl = [half_h, half_v, -half_h, -half_v] + else: + self.trbl = [half_v, half_h, -half_v, -half_h] + def projection_matrix(self): + t, r, b, l = self.trbl + n, f = self.clip + + #OpenGL projection matrices + #http://www.songho.ca/opengl/gl_projectionmatrix.html + if not self.perspective: + return np.asarray( + [[2/(r-l),0,0,0], + [0,2/(t-b),0,0], + [0,0,-2/(f-n),0], + [-(r+l)/(r-l),-(t+b)/(t-b),-(f+n)/(f-n),1]], + dtype=np.float32).transpose() + # https://www.scratchapixel.com/lessons/3d-basic-rendering/perspective-and-orthographic-projection-matrix/building-basic-perspective-projection-matrix + #https://www.scratchapixel.com/lessons/3d-basic-rendering/perspective-and-orthographic-projection-matrix/opengl-perspective-projection-matrix + else: + return np.asarray( + [[2*n/(r-l), 0, 0, 0], + [0, 2*n/(t-b), 0, 0], + [(r+l)/(r-l), (t+b)/(t-b), -(f+n)/(f-n), -1], + [0, 0, -2*f*n/(f-n), 0]], + dtype=np.float32).transpose() + + @property + def depth_step(self): + '''step size in view space''' + return (self.far - self.near)/self.transform.grid_size[0] + @property + def pix_height_near(self): + '''step size in view space''' + return (self.top - self.bottom)/self.transform.grid_size[1] + @property + def pix_width_near(self): + '''step size in view space''' + return (self.right - self.left)/self.transform.grid_size[2] + + @property + def position_global(self): + return self.transform.position_global() + @property + def forward_global(self): + return self.transform.forward_global() + @property + def up_global(self): + return self.transform.up_global() + @property + def right_global(self): + return self.transform.right_global() + @property + def near(self): + return self.clip[0] + @property + def far(self): + return self.clip[1] + @property + def top(self): + return self.trbl[0] + @property + def right(self): + return self.trbl[1] + @property + def bottom(self): + return self.trbl[2] + @property + def left(self): + return self.trbl[3] + @property + def view_height(self): + return self.top-self.bottom + @property + def view_width(self): + return self.right-self.left + + @property + def aspect(self): + return self.view_height/self.view_width #h/v + + def _near_plane_intersection(self, pos_view, near=None): + ''' https://en.wikipedia.org/wiki/Line%E2%80%93plane_intersection + near plane: n = (0,0,-1), p0=(0,0,-near) + line: l0= (0,0,0), l= pos_view + ''' + if near==None: + near = self.near + pos_view = Float3(np.asarray(pos_view, dtype=np.float32)[:3]).normalized + n = Float3(0,0,-1) + den = np.dot(pos_view, n) + if den==0: + return None + #num = dot([0,0,-near],n) = near + #d= num/den + return pos_view*(near/den) + + def _project_on_near(self, pos_view, near=None): + if near==None: + near = self.near + pos_view = np.asarray(pos_view, dtype=np.float32)[:3] + #intercept theorem + scale = near/(-pos_view[2]) + return pos_view*scale + + def project_world_to_screenUV(self, pos_world): + if len(pos_world)==3: + pos_world = Float4(Float3(pos_world), 1) + mat_view = MatrixTransform(self.view_matrix(with_jitter=False)) + pos_view = mat_view.transform(pos_world) + pos_near = Float3(self._project_on_near(pos_view)) + pos_uv = (pos_near.xy - Float2(self.left, self.bottom)) / Float2(self.view_width, self.view_height) + return pos_uv + + def project_world_to_screenPIX(self, pos_world): + pos_uv = self.project_world_to_screenUV(pos_world) + return pos_uv * Float2(self.transform.grid_size[2], self.transform.grid_size[1]) + + def screenUV_to_worldRay(self, pos_uv): + ''' + return a ray, starting at pos on near plane + ''' + mat_view_inv = self.view_transform_inverse(with_jitter=False) + pos_near = (pos_uv * Float2(self.view_width, self.view_height)) + Float2(self.left, self.bottom) + pos_near = Float4(pos_near, -self.near, 1) #in view space (VS) + pos_world = mat_view_inv.transform(pos_near).xyz + dir_world = (pos_world - Float4(self.position_global).xyz).normalized + return pos_world, dir_world + + def screenPIX_to_worldRay(self, pos_pix): + ''' + return a ray, starting at pos on near plane + ''' + pos_uv = pos_pix / Float2(self.transform.grid_size[2], self.transform.grid_size[1]) + return self.screenUV_to_worldRay(pos_uv) + + def copy_clipped_to_world_coords_old(self, coordinate_list, preserve_aspect=True, pad=0.0, clip_to_original=True): + ''' + copy camera with trbl set to the minimum hull of the original trbl and the projected coordinates (like scissor rect) + does not change resolution -> higher spacial resolution + coordinate_list: list of 3D world-space coorinates as 4D verctors (with w=1) + ''' + #raise NotImplementedError + # get x,y in view space in near plane + view = MatrixTransform(self.view_matrix()) + pos_view = [] + pos_view_z = [] + for p in coordinate_list: + p_view = view.transform(p) + pos_view.append(p_view) + pos_view_z.append(p_view[2]) + pos_view_z = np.asarray(pos_view_z) + + near = self.near #np.amin(-pos_view_z)-pad + far = self.far #np.amax(-pos_view_z)+pad + if clip_to_original: + near = np.clip(near, self.near, self.far) + far = np.clip(far , self.near, self.far) + + pos_near = [] + for p_view in pos_view: + p_near = self._near_plane_intersection(p_view, near) + if p_near is not None: + pos_near.append(p_near) + if len(pos_near)==0: + raise ValueError("") + x,y,_ = np.split(np.asarray(pos_near), 3, axis=-1) + + # project original trbl to new near for comparison + pr, pt, _ = self._near_plane_intersection([self.right,self.top,-self.near], near) + pl, pb, _ = self._near_plane_intersection([self.left,self.bottom,-self.near], near) + + # min/max considering (projected) original trbl + t = np.amax(y)+pad + r = np.amax(x)+pad + b = np.amin(y)-pad + l = np.amin(x)-pad + if clip_to_original: + t = np.clip(t, pb, pt) + r = np.clip(r, pl, pr) + b = np.clip(b, pb, pt) + l = np.clip(l, pl, pr) + + # perserve aspect ratio + if preserve_aspect: + aspect = self.aspect + h = t-b + v = r-l + a_v = h/aspect + if a_v higher spacial resolution + coordinate_list: list of 3D world-space coorinates as 4D verctors (with w=1) + preserve_clip: don't move near/far clip, preserves sampling step size + ''' + if len(coordinate_list)==0: + raise ValueError("Empty coordinate_list") + #raise NotImplementedError + # get x,y in view space in near plane + view = MatrixTransform(self.view_matrix()) + pos_view = [] + pos_view_z = [] + for p in coordinate_list: + p_view = view.transform(p) + pos_view.append(p_view) + pos_view_z.append(p_view[2]) + pos_view_z = np.asarray(pos_view_z) + + if not preserve_clip: + near = np.amin(-pos_view_z)-pad + far = np.amax(-pos_view_z)+pad + if clamp_to_original: + near = np.clip(near, self.near, self.far) + far = np.clip(far , self.near, self.far) + else: + near = self.near + far = self.far + + #project frustum corners to new near + n_scale = near / self.near + pt = self.top * n_scale + pr = self.right * n_scale + pb = self.bottom * n_scale + pl = self.left * n_scale + + pos_near = [] + for p_view in pos_view: + pos_near.append(self._project_on_near(p_view, near)) + x,y,_ = np.split(np.asarray(pos_near), 3, axis=-1) + + # min/max considering (projected) original trbl + t = np.amax(y)+pad + r = np.amax(x)+pad + b = np.amin(y)-pad + l = np.amin(x)-pad + if clamp_to_original: + t = np.clip(t, pb, pt) + r = np.clip(r, pl, pr) + b = np.clip(b, pb, pt) + l = np.clip(l, pl, pr) + + # perserve aspect ratio + if preserve_aspect: + aspect = self.aspect + h = t-b + v = r-l + a_v = h/aspect + if a_v +#include +//#define LOGGING +#include "advect.hpp" +#include "render_errors.hpp" + +#ifdef LOGGING +#define MYLOG(msg) std::cout << __FILE__ << "[" << __LINE__ << "]: " << msg << std::endl +#define LOG_PRINTF(msg) printf(msg) +#else +#define MYLOG(msg) +#define LOG_PRINTF(msg) +#endif +using namespace tensorflow; + +REGISTER_OP("AdvectGridSemiLangrange") + .Input("input: float") // NDHWC + .Input("velocity_centered: float") //VDHWC, defines output shape + .Attr("timestep: float = 0.0") + //.Attr("interpolation: {'NEAREST', 'LINEAR', 'MIN', 'MAX'} = 'LINEAR'") + .Attr("order: int >= 1 = 1") + .Attr("clamp_extrema: bool = true") + .Attr("boundary: {'BORDER', 'CLAMP', 'WRAP'} = 'BORDER'") + //.Attr("mipmapping: {'NONE', 'NEAREST', 'LINEAR'} = 'NONE'") + //.Attr("num_mipmaps: int = 0") + //.Attr("mip_bias: float = 0.0") + .Attr("separate_velocity_batch: bool = true") + //.Attr("relative_coords: bool = true") + //.Attr("normalized_coords: bool = false") + .Output("output: float") // NVDHWC + .SetShapeFn([](::tensorflow::shape_inference::InferenceContext* c) + { + ::tensorflow::shape_inference::ShapeHandle channel; + TF_RETURN_IF_ERROR(c->Subshape(c->input(0), 3, &channel)); + ::tensorflow::shape_inference::ShapeHandle outShape; + TF_RETURN_IF_ERROR(c->Subshape(c->input(1), 0, 3, &outShape)); + TF_RETURN_IF_ERROR(c->Concatenate(outShape, channel, &outShape)); + c->set_output(0, outShape); + return Status::OK(); + }); + + +// the gradient op +REGISTER_OP("AdvectGridSemiLangrangeGrad") + .Input("input: float") // NDHWC + .Input("output_grad: float") // NVDHWC + .Input("velocity_centered: float") //VDHWC + .Attr("timestep: float = 0.0") + //.Attr("interpolation: {'NEAREST', 'LINEAR', 'MIN', 'MAX'} = 'LINEAR'") + .Attr("order: int >= 1 = 1") + .Attr("clamp_extrema: bool = true") + .Attr("boundary: {'BORDER', 'CLAMP', 'WRAP'} = 'BORDER'") + //.Attr("mipmapping: {'NONE'} = 'NONE'") //, 'NEAREST', 'LINEAR'. currently not supported + //.Attr("num_mipmaps: int = 0") + //.Attr("mip_bias: float = 0.0") + .Attr("separate_velocity_batch: bool = true") + //.Attr("relative_coords: bool = true") + //.Attr("normalized_coords: bool = false") + .Output("input_grad: float") + .Output("velocity_grad: float") + .SetShapeFn([](::tensorflow::shape_inference::InferenceContext* c) + { + c->set_output(0, c->input(0)); + c->set_output(1, c->input(2)); + return Status::OK(); + }); + +#if GOOGLE_CUDA +#define DECLARE_GPU_SPEC(T, C) \ + template<> \ + void AdvectGridKernel::operator()( \ + const GPUDevice& d, \ + const T* input, const long long int* input_shape, \ + const float* velocity, T* tmp_fwd, T* tmp_min, T* tmp_max, \ + const float timestep, const int32_t order, const Sampling::BoundaryMode boundaryMode, \ + const bool revertExtrema, const int32_t numVelocities, const bool globalSampling, \ + T* output, const long long int* output_shape); \ + extern template struct AdvectGridKernel; +DECLARE_GPU_SPEC(float, 1) +DECLARE_GPU_SPEC(float, 2) +DECLARE_GPU_SPEC(float, 4) +#undef DECLARE_GPU_SPEC +#endif + + +template +class AdvectGridSemiLangrangeOp : public OpKernel{ +public: + explicit AdvectGridSemiLangrangeOp(OpKernelConstruction *context) : OpKernel(context){ + + /* + memset(&m_samplingSettings, 0, sizeof(Sampling::SamplerSettings)); + std::string s_interpolation; + OP_REQUIRES_OK(context, context->GetAttr("interpolation", &s_interpolation)); + if(s_interpolation.compare("LINEAR")==0) m_samplingSettings.filterMode = Sampling::SamplerSettings::FILTERMODE_LINEAR; + else if(s_interpolation.compare("MIN")==0) m_samplingSettings.filterMode = Sampling::SamplerSettings::FILTERMODE_MIN; + else if(s_interpolation.compare("MAX")==0) m_samplingSettings.filterMode = Sampling::SamplerSettings::FILTERMODE_MAX; + */ + + std::string s_boundary; + OP_REQUIRES_OK(context, context->GetAttr("boundary", &s_boundary)); + if(s_boundary.compare("CLAMP")==0) m_boundaryMode = Sampling::BOUNDARY_CLAMP; + else if(s_boundary.compare("WRAP")==0) m_boundaryMode = Sampling::BOUNDARY_WRAP; + else if(s_boundary.compare("BORDER")==0) m_boundaryMode = Sampling::BOUNDARY_BORDER; + else throw errors::InvalidArgument("Invalid boundary argument."); + + OP_REQUIRES_OK(context, context->GetAttr("timestep", &m_timestep)); + OP_REQUIRES_OK(context, context->GetAttr("order", &m_order)); + OP_REQUIRES(context, m_order==1 || m_order==2, + errors::InvalidArgument("Only first (SemiLangrange) and second (MacCormack) order advection supported.")); + OP_REQUIRES_OK(context, context->GetAttr("clamp_extrema", &m_clampExtrema)); + + /* + std::string s_mipmapping; + OP_REQUIRES_OK(context, context->GetAttr("mipmapping", &s_mipmapping)); + if(s_mipmapping.compare("NEAREST")==0) m_samplingSettings.mipMode = Sampling::SamplerSettings::MIPMODE_NEAREST; + else if(s_mipmapping.compare("LINEAR")==0) m_samplingSettings.mipMode = Sampling::SamplerSettings::MIPMODE_LINEAR; + + OP_REQUIRES_OK(context, context->GetAttr("num_mipmaps", &m_samplingSettings.mipLevel)); + OP_REQUIRES(context, m_samplingSettings.mipLevel>0 || m_samplingSettings.mipMode==Sampling::SamplerSettings::MIPMODE_NONE, + errors::InvalidArgument("when using mipmaps num_mipmaps must be larger than 0.")); + + OP_REQUIRES_OK(context, context->GetAttr("mip_bias", &m_samplingSettings.mipBias)); + */ + + OP_REQUIRES_OK(context, context->GetAttr("separate_velocity_batch", &m_globalSampling)); + + /* + OP_REQUIRES_OK(context, context->GetAttr("relative_coords", &m_relativeCoords)); + OP_REQUIRES_OK(context, context->GetAttr("normalized_coords", &m_normalizedCoords)); + if(m_normalizedCoords){ + OP_REQUIRES(context, m_samplingSettings.cellCenterOffset==0.5f, + errors::InvalidArgument("Invalid cell center offset must be 0.5 when using normalized coordinates.")); + } + */ + } + + void Compute(OpKernelContext *context) override{ + + MYLOG("Sample transform op kernel start"); + + const Tensor& tensor_input = context->input(0); + const Tensor& tensor_velocity = context->input(1); + + //check input + MYLOG("Check input"); + TensorShape input_shape = tensor_input.shape(); + OP_REQUIRES(context, tensor_input.dims()==5 && input_shape.dim_size(4)<=4, + errors::InvalidArgument("Invalid input shape (NDHWC):", input_shape.DebugString())); + const int64 batch = input_shape.dim_size(0); + const int64 channel = input_shape.dim_size(4); + + MYLOG("Check velocity"); + TensorShape velocity_shape = tensor_velocity.shape(); + OP_REQUIRES(context, velocity_shape.dims()==5 && velocity_shape.dim_size(4)==3, + errors::InvalidArgument("Invalid velocity shape (VDHWC) with C=3:", velocity_shape.DebugString())); + int32_t numVelocities = velocity_shape.dim_size(0); + if(m_order==2){ + OP_REQUIRES(context, + velocity_shape.dim_size(1)==input_shape.dim_size(1) && + velocity_shape.dim_size(2)==input_shape.dim_size(2) && + velocity_shape.dim_size(3)==input_shape.dim_size(3), + errors::InvalidArgument("Spatial dimensions of input and velocity must match when using 2nd order advection:", input_shape.DebugString(), velocity_shape.DebugString())); + } + + //create output shape + TensorShape output_shape; + { + MYLOG("Create output shape"); + output_shape.AddDim(velocity_shape.dim_size(1)); + output_shape.AddDim(velocity_shape.dim_size(2)); + output_shape.AddDim(velocity_shape.dim_size(3)); + output_shape.InsertDim(0,batch); + output_shape.AddDim(channel); + MYLOG("output_shape: " << output_shape.dim_size(0) << ", " << output_shape.dim_size(1) << ", " << output_shape.dim_size(2) << ", " << output_shape.dim_size(3)); + + if(!m_globalSampling){ + OP_REQUIRES(context, numVelocities==batch, errors::InvalidArgument("velocity batch must match data batch when not using global sampling.")); + output_shape.InsertDim(1,1); + }else{ + output_shape.InsertDim(1,numVelocities); + } + } + + + //allocate outout + Tensor* tensor_output = nullptr; + MYLOG("Allocate output"); + OP_REQUIRES_OK(context, context->allocate_output(0, output_shape, &tensor_output)); + MYLOG("Check allocated output\n"); + MYLOG("Allocated output size: " << tensor_output->flat().size() << " - " << tensor_output->NumElements()); + + //allocate temporary grids + TensorShape tmp_shape; + { + tmp_shape.AddDim(1); + tmp_shape.AddDim(velocity_shape.dim_size(1)); + tmp_shape.AddDim(velocity_shape.dim_size(2)); + tmp_shape.AddDim(velocity_shape.dim_size(3)); + tmp_shape.AddDim(channel); + } + Tensor tensor_fwd; + T* p_tensor_fwd = nullptr; + Tensor tensor_min; + T* p_tensor_min = nullptr; + Tensor tensor_max; + T* p_tensor_max = nullptr; + if(m_order==2){ + OP_REQUIRES_OK(context, context->allocate_temp(DT_FLOAT, tmp_shape, &tensor_fwd)); + p_tensor_fwd = tensor_fwd.flat().data(); + if(m_clampExtrema){ + OP_REQUIRES_OK(context, context->allocate_temp(DT_FLOAT, tmp_shape, &tensor_min)); + p_tensor_min = tensor_min.flat().data(); + OP_REQUIRES_OK(context, context->allocate_temp(DT_FLOAT, tmp_shape, &tensor_max)); + p_tensor_max = tensor_max.flat().data(); + } + } + + + + //TODO handle arbitrary amount of channel + // - move channel dimension outwards (to batch) and handle only 1-channel case internally. would also handle batches + // this would benefit from NCHW layout, otherwise have to transpose + // or just require NCHW as input format (or NHWC with up to 4 channel, the rest has to be packed in N) and let tensorflow/user handle the conversion... + // - split into up-to-4-channel partitions. might be faster? but is harder to handle + + MYLOG("Resample"); + switch(channel){ + case 1: + AdvectGridKernel()(tensor_input.flat().data(), input_shape.dim_sizes().data(), + tensor_velocity.flat().data(), p_tensor_fwd, p_tensor_min, p_tensor_max, + m_timestep, m_order, m_boundaryMode, m_clampExtrema, + numVelocities, m_globalSampling, + tensor_output->flat().data(), output_shape.dim_sizes().data()); + break; + case 2: + AdvectGridKernel()(tensor_input.flat().data(), input_shape.dim_sizes().data(), + tensor_velocity.flat().data(), p_tensor_fwd, p_tensor_min, p_tensor_max, + m_timestep, m_order, m_boundaryMode, m_clampExtrema, + numVelocities, m_globalSampling, + tensor_output->flat().data(), output_shape.dim_sizes().data()); + break; + case 4: + AdvectGridKernel()(tensor_input.flat().data(), input_shape.dim_sizes().data(), + tensor_velocity.flat().data(), p_tensor_fwd, p_tensor_min, p_tensor_max, + m_timestep, m_order, m_boundaryMode, m_clampExtrema, + numVelocities, m_globalSampling, + tensor_output->flat().data(), output_shape.dim_sizes().data()); + break; + default: + OP_REQUIRES(context, false, + errors::Unimplemented("Only 1,2 and 4 Channel data supported.")); + } + + MYLOG("Kernel done"); + } +private: + bool m_globalSampling; + float m_timestep; + int32_t m_order; + bool m_clampExtrema; + Sampling::BoundaryMode m_boundaryMode; + //bool m_relativeCoords; + //bool m_normalizedCoords; + +}; + +#if 0 + +#if GOOGLE_CUDA +#define DECLARE_GPU_SPEC(T, C) \ + template<> \ + void AdvectGridGradKernel::operator()( \ + const GPUDevice& d, \ + const void* input, const long long int* input_shape, \ + const float* M, const float* V, const float* P, const float* frustum, int32_t numCameras, \ + uint8_t* mipAtlas, \ + const Sampling::CoordinateMode coordinateMode, \ + const Sampling::SamplerSettings, const bool globalSampling, \ + void* output, const long long int* output_shape \ + ); \ + extern template struct AdvectGridGradKernel; +DECLARE_GPU_SPEC(float, 1) +DECLARE_GPU_SPEC(float, 2) +DECLARE_GPU_SPEC(float, 4) +#undef DECLARE_GPU_SPEC +#endif + + +template +class AdvectGridSemiLangrangeGradOp : public OpKernel{ +public: + explicit AdvectGridSemiLangrangeGradOp(OpKernelConstruction *context) : OpKernel(context){ + + /* + memset(&m_samplingSettings, 0, sizeof(Sampling::SamplerSettings)); + std::string s_interpolation; + OP_REQUIRES_OK(context, context->GetAttr("interpolation", &s_interpolation)); + if(s_interpolation.compare("LINEAR")==0) m_samplingSettings.filterMode = Sampling::SamplerSettings::FILTERMODE_LINEAR; + else if(s_interpolation.compare("MIN")==0) m_samplingSettings.filterMode = Sampling::SamplerSettings::FILTERMODE_MIN; + else if(s_interpolation.compare("MAX")==0) m_samplingSettings.filterMode = Sampling::SamplerSettings::FILTERMODE_MAX; + */ + + std::string s_boundary; + OP_REQUIRES_OK(context, context->GetAttr("boundary", &s_boundary)); + if(s_boundary.compare("CLAMP")==0) m_samplingSettings.boundaryMode = Sampling::SamplerSettings::CLAMP; + else if(s_boundary.compare("WRAP")==0) m_samplingSettings.boundaryMode = Sampling::SamplerSettings::WRAP; + else if(s_boundary.compare("MIRROR")==0) m_samplingSettings.boundaryMode = Sampling::SamplerSettings::MIRROR; + else throw errors::InvalidArgument("Invalid boundary argument."); + + OP_REQUIRES_OK(context, context->GetAttr("timestep", &m_timestep)); + OP_REQUIRES_OK(context, context->GetAttr("order", &m_order)); + OP_REQUIRES(context, m_order==1 || m_order==2, + errors::InvalidArgument("Only first (SemiLangrange) and second (MacCormack) order advection supported.")); + OP_REQUIRES_OK(context, context->GetAttr("clamp_extrema", &m_clampExtrema)); + + /* + std::string s_mipmapping; + OP_REQUIRES_OK(context, context->GetAttr("mipmapping", &s_mipmapping)); + if(s_mipmapping.compare("NEAREST")==0) m_samplingSettings.mipMode = Sampling::SamplerSettings::MIPMODE_NEAREST; + else if(s_mipmapping.compare("LINEAR")==0) m_samplingSettings.mipMode = Sampling::SamplerSettings::MIPMODE_LINEAR; + + OP_REQUIRES_OK(context, context->GetAttr("num_mipmaps", &m_samplingSettings.mipLevel)); + OP_REQUIRES(context, m_samplingSettings.mipLevel>0 || m_samplingSettings.mipMode==Sampling::SamplerSettings::MIPMODE_NONE, + errors::InvalidArgument("when using mipmaps num_mipmaps must be larger than 0.")); + + OP_REQUIRES_OK(context, context->GetAttr("mip_bias", &m_samplingSettings.mipBias)); + */ + + OP_REQUIRES_OK(context, context->GetAttr("separate_velocity_batch", &m_globalSampling)); + + /* + OP_REQUIRES_OK(context, context->GetAttr("relative_coords", &m_relativeCoords)); + OP_REQUIRES_OK(context, context->GetAttr("normalized_coords", &m_normalizedCoords)); + if(m_normalizedCoords){ + OP_REQUIRES(context, m_samplingSettings.cellCenterOffset==0.5f, + errors::InvalidArgument("Invalid cell center offset must be 0.5 when using normalized coordinates.")); + } + */ + } + + void Compute(OpKernelContext *context) override{ + + MYLOG("Sample transform op kernel start"); + + const Tensor& tensor_input = context->input(0); + const Tensor& tensor_grad_output = context->input(1); + const Tensor& tensor_velocity = context->input(2); + + //check input + MYLOG("Check input"); + TensorShape input_shape = tensor_input.shape(); + OP_REQUIRES(context, tensor_input.dims()==5 && input_shape.dim_size(4)<=4, + errors::InvalidArgument("Invalid input shape (NDHWC):", input_shape.DebugString())); + const int64 batch = input_shape.dim_size(0); + const int64 channel = input_shape.dim_size(4); + + MYLOG("Check velocity"); + TensorShape velocity_shape = tensor_velocity.shape(); + OP_REQUIRES(context, velocity_shape.dims()==5 && velocity_shape.dim_size(4)==3, + errors::InvalidArgument("Invalid velocity shape (VDHWC) with C=3:", velocity_shape.DebugString())); + int32_t numVelocities = velocity_shape.dim_size(0); + if(m_order==2){ + OP_REQUIRES(context, + velocity_shape.dim_size(1)==input_shape.dim_size(1) && + velocity_shape.dim_size(2)==input_shape.dim_size(2) && + velocity_shape.dim_size(3)==input_shape.dim_size(3), + errors::InvalidArgument("Spatial dimensions of input and velocity must match when using 2nd order advection:", input_shape.DebugString(), velocity_shape.DebugString())); + } + + //check output shape + TensorShape output_shape; + { + MYLOG("Create output shape"); + output_shape.AddDim(velocity_shape.dim_size(1)); + output_shape.AddDim(velocity_shape.dim_size(2)); + output_shape.AddDim(velocity_shape.dim_size(3)); + output_shape.InsertDim(0,batch); + output_shape.AddDim(channel); + MYLOG("output_shape: " << output_shape.dim_size(0) << ", " << output_shape.dim_size(1) << ", " << output_shape.dim_size(2) << ", " << output_shape.dim_size(3)); + + if(!m_globalSampling){ + OP_REQUIRES(context, numVelocities==batch, errors::InvalidArgument("velocity batch must match data batch when not using global sampling.")); + output_shape.InsertDim(1,1); + }else{ + output_shape.InsertDim(1,numVelocities); + } + } + + + //allocate outout + Tensor* tensor_output = nullptr; + MYLOG("Allocate output"); + OP_REQUIRES_OK(context, context->allocate_output(0, output_shape, &tensor_output)); + MYLOG("Check allocated output\n"); + MYLOG("Allocated output size: " << tensor_output->flat().size() << " - " << tensor_output->NumElements()); + + //allocate temporary grids + TensorShape tmp_shape; + { + tmp_shape.AddDim(1); + tmp_shape.AddDim(velocity_shape.dim_size(1)); + tmp_shape.AddDim(velocity_shape.dim_size(2)); + tmp_shape.AddDim(velocity_shape.dim_size(3)); + tmp_shape.AddDim(channel); + } + Tensor tensor_fwd; + T* p_tensor_fwd = nullptr; + Tensor tensor_fwd_grad; + T* p_tensor_fwd_grad = nullptr; + Tensor tensor_min; + T* p_tensor_min = nullptr; + Tensor tensor_max; + T* p_tensor_max = nullptr; + if(m_order==2){ + OP_REQUIRES_OK(context, context->allocate_temp(T, tmp_shape, &tensor_fwd)); + p_tensor_fwd = tensor_fwd.flat().data(); + OP_REQUIRES_OK(context, context->allocate_temp(T, tmp_shape, &tensor_fwd_grad)); + p_tensor_fwd_grad = tensor_fwd_grad.flat().data(); + if(m_clampExtrema){ + OP_REQUIRES_OK(context, context->allocate_temp(T, tmp_shape, &tensor_min)); + p_tensor_min = tensor_min.flat().data(); + OP_REQUIRES_OK(context, context->allocate_temp(T, tmp_shape, &tensor_max)); + p_tensor_max = tensor_max.flat().data(); + } + } + + + + //TODO handle arbitrary amount of channel + // - move channel dimension outwards (to batch) and handle only 1-channel case internally. would also handle batches + // this would benefit from NCHW layout, otherwise have to transpose + // or just require NCHW as input format (or NHWC with up to 4 channel, the rest has to be packed in N) and let tensorflow/user handle the conversion... + // - split into up-to-4-channel partitions. might be faster? but is harder to handle + + MYLOG("Resample"); + switch(channel){ + case 1: + AdvectGridGradKernel()(tensor_input.flat().data(), input_shape.dim_sizes().data(), + tensor_velocity.flat().data(), p_tensor_fwd, p_tensor_min, p_tensor_max, + m_timestep, m_order, m_clampExtrema + numVelocities, m_globalSampling, + output_grid->flat().data(), output_shape.dim_sizes().data()); + break; + case 2: + AdvectGridGradKernel()(tensor_input.flat().data(), input_shape.dim_sizes().data(), + tensor_velocity.flat().data(), p_tensor_fwd, p_tensor_min, p_tensor_max, + m_timestep, m_order, m_clampExtrema + numVelocities, m_globalSampling, + output_grid->flat().data(), output_shape.dim_sizes().data()); + break; + case 4: + AdvectGridGradKernel()(tensor_input.flat().data(), input_shape.dim_sizes().data(), + tensor_velocity.flat().data(), p_tensor_fwd, p_tensor_min, p_tensor_max, + m_timestep, m_order, m_clampExtrema + numVelocities, m_globalSampling, + output_grid->flat().data(), output_shape.dim_sizes().data()); + break; + default: + OP_REQUIRES(context, false, + errors::Unimplemented("Only 1,2 and 4 Channel data supported.")); + } + + MYLOG("Kernel done"); + } +private: + bool m_globalSampling; + float m_timestep; + int32_t m_order; + bool m_clampExtrema; + //bool m_relativeCoords; + //bool m_normalizedCoords; + +}; + +#endif //0 diff --git a/phitest/render/cuda/src/advect.cu.cc b/phitest/render/cuda/src/advect.cu.cc new file mode 100644 index 0000000..185d544 --- /dev/null +++ b/phitest/render/cuda/src/advect.cu.cc @@ -0,0 +1,389 @@ + +#if GOOGLE_CUDA +#define EIGEN_USE_GPU +//#include "tensorflow/core/lib/core/errors.h" +//#include "tensorflow/core/platform/errors.h" +//#include "tensorflow/core/framework/op_kernel.h" +#include +#include "cuda-samples/Common/helper_cuda.h" +#include +#include +#include "render_errors.hpp" +//#define LOGGING + +#ifdef LOGGING +#define PROFILING +#endif + +#ifdef PROFILING +//#include +#include +#endif + + +#include "vectormath.hpp" +#include "vector_io.hpp" + +//kernel_setup params +#define BLOCK_SIZE_X 16 +#define BLOCK_SIZE_Y 4 +#define BLOCK_SIZE_Z 4 +//define BLOCK_SIZE BLOCK_SIZE_X*BLOCK_SIZE_Y*BLOCK_SIZE_Z +//#define BLOCK_DIMS BLOCK_SIZE_X, BLOCK_SIZE_Y, BLOCK_SIZE_Z +#include "kernel_setup.hpp" +#include "advect.hpp" +#include "sampling_v2.hpp" + +//#include "sampling_v2.hpp" + +inline __device__ int3 make_globalThreadIdx(){ + return make_int3(blockIdx.x*blockDim.x + threadIdx.x, blockIdx.y*blockDim.y + threadIdx.y, blockIdx.z*blockDim.z + threadIdx.z); + //return make_int3(blockIdx*blockDim + threadIdx); +} + + +struct AdvectionConstants{ + int3 dimensionsInput; + int3 dimensionsOutput; + int32_t batch; + int32_t channel; + float timestep; + bool revertExtrema; +}; +__constant__ AdvectionConstants c_adv; + + +// --- SemiLangrange Advection --- + +template +__global__ void +__launch_bounds__(BLOCK_SIZE) +kAdvectGrid3DSemiLangrange(const T* UG_PTR input, const float3* UG_PTR velocity, T* UG_PTR output, T* UG_PTR sampledMin, T* UG_PTR sampledMax){ + int3 globalIdx = make_globalThreadIdx(); + if(isInDimensions(globalIdx, c_adv.dimensionsOutput)){ + float3 samplePos = vectorIO::readVectorType3D(globalIdx, c_adv.dimensionsOutput, velocity); + samplePos *= c_adv.timestep; + samplePos += make_float3(globalIdx); + + T data; + if(EXTREMA){ //sample with extrema + Sampling::DataWithExtrema dataWithExtrema = Sampling::read3DInterpolatedWithExtrema(samplePos, input, c_adv.dimensionsInput, 0); + data = dataWithExtrema.data; + vectorIO::writeVectorType3D(data, globalIdx, c_adv.dimensionsOutput, sampledMin); + vectorIO::writeVectorType3D(data, globalIdx, c_adv.dimensionsOutput, sampledMax); + }else{ //sample without extrema + data = Sampling::read3DInterpolated(samplePos, input, c_adv.dimensionsInput, 0); + } + vectorIO::writeVectorType3D(data, globalIdx, c_adv.dimensionsOutput, output); + } +} + +/* + * input (grad): [depth, height, width, channel (1-4)] + * lut (grad): [depth, height, width, channel (4)], channel: (abs_x, abs_y, abs_z, LoD) +https://github.com/tensorflow/tensorflow/blob/r1.12/tensorflow/contrib/resampler/kernels/resampler_ops_gpu.cu.cc +*/ +template +__global__ void +__launch_bounds__(BLOCK_SIZE) +kAdvectGrid3DSemiLangrangeGradients(const T* UG_PTR input, const T* UG_PTR output_grad, const float3* UG_PTR velocity, T* UG_PTR input_grad, float3* UG_PTR velocity_grad){ + int3 globalIdx = make_globalThreadIdx(); + if(isInDimensions(globalIdx, c_adv.dimensionsOutput)){ + float3 samplePos = vectorIO::readVectorType3D(globalIdx, c_adv.dimensionsOutput, velocity); + samplePos *= c_adv.timestep; + samplePos += float3(globalIdx, 0.0); + + // + const T out_grad = vectorIO::readVectorType3D(globalIdx, c_adv.dimensionsOutput, output_grad); + + //backprop gradients to velocity, including timestep + Sampling::DataGrad3D dataGrad = Sampling::read3DGrad(samplePos, input, c_adv.dimensionsInput); + const float3 vel_grad = c_adv.timestep * make_float3(vmath::sum(out_grad*dataGrad.dx), vmath::sum(out_grad*dataGrad.dy), vmath::sum(out_grad*dataGrad.dz)); + if(GRADIENT_ADDITIVE){ + vectorIO::addVectorType3D(vel_grad, globalIdx, c_adv.dimensionsOutput, velocity_grad); + }else{ + vectorIO::writeVectorType3D(vel_grad, globalIdx, c_adv.dimensionsOutput, velocity_grad); + } + + Sampling::scatterGrad3DInterpolated(out_grad, samplePos, input_grad); + } +} + +// --- MacCormack correction step --- +// after a forward and a backward SemiLangrangian step +/* +template +__global__ void +__launch_bounds__(BLOCK_SIZE) +kCorrectGrid3DMacCormack(const T* input, const T* fwd, T* output, const T* sampledMin, const T* sampledMax, bool revertExtrema){ + int3 globalIdx = make_globalThreadIdx(); + if(isInDimensions(globalIdx, c_adv.dimensionsOutput)){ + const T dataIn = vectorIO::readVectorType3D(globalIdx, c_adv.dimensionsOutput, input); + const T dataFwd = vectorIO::readVectorType3D(globalIdx, c_adv.dimensionsOutput, fwd); + const T dataBwd = vectorIO::readVectorType3D(globalIdx, c_adv.dimensionsOutput, output); + + T corrected = dataFwd + (dataIn - dataBwd) * 0.5f; + + if(revertExtrema){ + const T dataMin = vectorIO::readVectorType3D(globalIdx, c_adv.dimensionsOutput, sampledMin); + const T dataMax = vectorIO::readVectorType3D(globalIdx, c_adv.dimensionsOutput, sampledMax); + + /* + if (corrected(corrected, globalIdx, c_adv.dimensionsOutput, output); + } +} + +template +__global__ void +__launch_bounds__(BLOCK_SIZE) +kCorrectGrid3DMacCormackGradients(T* input_grads, T* fwd_grads, const T* output_grads, const T* sampledMin, const T* sampledMax, bool revertExtrema){ + int3 globalIdx = make_globalThreadIdx(); + if(isInDimensions(globalIdx, c_adv.dimensionsOutput)){ + const T gradOut = vectorIO::readVectorType3D(globalIdx, c_adv.dimensionsOutput, output_grads); + T gradFwd; + T gradCorrected; + + if(revertExtrema){ + const T dataMin = vectorIO::readVectorType3D(globalIdx, c_adv.dimensionsOutput, sampledMin); + const T dataMax = vectorIO::readVectorType3D(globalIdx, c_adv.dimensionsOutput, sampledMax); + + T t = (corrected(gradIn, globalIdx, c_adv.dimensionsOutput, input_grads); + vectorIO::writeVectorType3D(gradFwd, globalIdx, c_adv.dimensionsOutput, fwd_grads); + vectorIO::writeVectorType3D(gradBwd, globalIdx, c_adv.dimensionsOutput, bwd_grads); + } +} +*/ +// --- MacCormack Step (fused)--- +// combines SemiLangrangian backward step and MacCormack correction, after a SL forward step + +template +__global__ void +__launch_bounds__(BLOCK_SIZE) +kAdvectCorrectGrid3DMacCormack(const T* UG_PTR input, const T* UG_PTR fwd, const T* UG_PTR sampledMin, const T* UG_PTR sampledMax, const float3* UG_PTR velocity, T* UG_PTR output){ + int3 globalIdx = make_globalThreadIdx(); + if(isInDimensions(globalIdx, c_adv.dimensionsOutput)){ + float3 samplePosBwd = vectorIO::readVectorType3D(globalIdx, c_adv.dimensionsOutput, velocity); + const T dataIn = vectorIO::readVectorType3D(globalIdx, c_adv.dimensionsInput, input); + const T dataFwd = vectorIO::readVectorType3D(globalIdx, c_adv.dimensionsOutput, fwd); + samplePosBwd *= - c_adv.timestep; + samplePosBwd += make_float3(globalIdx); + const T dataBwd = Sampling::read3DInterpolated(samplePosBwd, fwd, c_adv.dimensionsOutput, 0); + + T corrected = dataFwd + (dataIn - dataBwd) * 0.5f; + + if(c_adv.revertExtrema){ + const T dataMin = vectorIO::readVectorType3D(globalIdx, c_adv.dimensionsOutput, sampledMin); + const T dataMax = vectorIO::readVectorType3D(globalIdx, c_adv.dimensionsOutput, sampledMax); + + /* + if (corrected(corrected, dataFwd, (corrected(corrected, globalIdx, c_adv.dimensionsOutput, output); + } +} + +template +__global__ void +__launch_bounds__(BLOCK_SIZE) +kAdvectCorrectGrid3DMacCormackGradients(const T* UG_PTR input, const T* UG_PTR output_grad, const T* UG_PTR fwd, const T* UG_PTR sampledMin, const T* UG_PTR sampledMax, const float3* UG_PTR velocity, + T* UG_PTR input_grad, T* UG_PTR fwd_grad, float3* UG_PTR velocity_grad){ + + int3 globalIdx = make_globalThreadIdx(); + if(isInDimensions(globalIdx, c_adv.dimensionsOutput)){ + float3 samplePosBwd = vectorIO::readVectorType3D(globalIdx, c_adv.dimensionsOutput, velocity); + samplePosBwd *= -c_adv.timestep; + samplePosBwd += float3(globalIdx, 0.0); + const T gradOut = vectorIO::readVectorType3D(globalIdx, c_adv.dimensionsOutput, output_grad); + T gradFwd; + T gradCorrected; + + if(c_adv.revertExtrema){ + const T dataMin = vectorIO::readVectorType3D(globalIdx, c_adv.dimensionsOutput, sampledMin); + const T dataMax = vectorIO::readVectorType3D(globalIdx, c_adv.dimensionsOutput, sampledMax); + + const T dataIn = vectorIO::readVectorType3D(globalIdx, c_adv.dimensionsOutput, input); + const T dataFwd = vectorIO::readVectorType3D(globalIdx, c_adv.dimensionsOutput, fwd); + const T dataBwd = Sampling::read3DInterpolated(samplePosBwd, fwd, c_adv.dimensionsOutput, 0); + const T corrected = dataFwd + (dataIn - dataBwd) * 0.5f; + + T t = (corrected dataGrad = Sampling::read3DGrad(samplePosBwd, fwd, c_adv.dimensionsOutput); + const float3 vel_grad = (-c_adv.timestep) * make_float3(vmath::sum(gradBwd*dataGrad.dx), vmath::sum(gradBwd*dataGrad.dy), vmath::sum(gradBwd*dataGrad.dz)); + vectorIO::writeVectorType3D(vel_grad, globalIdx, c_adv.dimensionsOutput, velocity_grad); + + vectorIO::writeVectorType3D(gradIn, globalIdx, c_adv.dimensionsOutput, input_grad); + //vectorIO::writeVectorType3D(gradFwd, globalIdx, c_adv.dimensionsOutput, fwd_grads); + //-> needs to be atomic to be compatible with bwd scatter + vectorIO::atomicAddVectorType3D(gradFwd, globalIdx, c_adv.dimensionsOutput, fwd_grad); + //vectorIO::writeVectorType3D(gradBwd, globalIdx, c_adv.dimensionsOutput, bwd_grads); + Sampling::scatterGrad3DInterpolated(gradBwd, samplePosBwd, fwd_grad); + } +} + +inline int3 dimensionsFromGridShape(const long long int* shape, uint32_t offset=1){ + return make_int3(shape[offset+2], shape[offset+1], shape[offset]); //default offset 1: NDHWC (zyx) -> WHD (xyz) +} + + +template +void AdvectGridKernelLauncher(const GPUDevice& d, + const T* input, const long long int* shape_input, + const float3* velocity, T* tmp_fwd, T* tmp_min, T* tmp_max, + const float timestep, const int32_t order, const Sampling::BoundaryMode boundaryMode, + const bool revertExtrema, const int32_t numVelocities, const bool globalSampling, + T* output, const long long int* shape_output + ){ + LOG("Begin AdvectGridKernelLauncher"); + + //setup constants + AdvectionConstants advectionConstants; + { + memset(&advectionConstants, 0, sizeof(AdvectionConstants)); + advectionConstants.dimensionsInput = dimensionsFromGridShape(shape_input,1); + advectionConstants.dimensionsOutput = dimensionsFromGridShape(shape_output,2); + advectionConstants.batch = shape_input[0]; + //advectionConstants.channel = shape_input[4]; + advectionConstants.timestep = timestep; + advectionConstants.revertExtrema = revertExtrema; + CUDA_CHECK_RETURN(cudaMemcpyToSymbol(c_adv, &advectionConstants, sizeof(AdvectionConstants))); + } + const size_t inputBatchSizeElements = vmath::prod(advectionConstants.dimensionsInput); + const size_t velocityBatchSizeElements = vmath::prod(advectionConstants.dimensionsOutput); + const size_t outputBatchSizeElements = velocityBatchSizeElements; + + + const dim3 grid(GRID_DIMS(advectionConstants.dimensionsOutput)); + const dim3 block(BLOCK_DIMS); + LOG("Advect " << batchSize << " grids with " << numVelocities << " velocities"); + + for(size_t batch=0; batch<<>>(currInput, currVel, currOut, tmp_min, tmp_max); + else if(boundaryMode==Sampling::BOUNDARY_CLAMP) + kAdvectGrid3DSemiLangrange<<>>(currInput, currVel, currOut, tmp_min, tmp_max); + else if(boundaryMode==Sampling::BOUNDARY_WRAP) + kAdvectGrid3DSemiLangrange<<>>(currInput, currVel, currOut, tmp_min, tmp_max); + }else{ + if(boundaryMode==Sampling::BOUNDARY_BORDER) + kAdvectGrid3DSemiLangrange<<>>(currInput, currVel, currOut, nullptr, nullptr); + else if(boundaryMode==Sampling::BOUNDARY_CLAMP) + kAdvectGrid3DSemiLangrange<<>>(currInput, currVel, currOut, nullptr, nullptr); + else if(boundaryMode==Sampling::BOUNDARY_WRAP) + kAdvectGrid3DSemiLangrange<<>>(currInput, currVel, currOut, nullptr, nullptr); + } + + if(order==2){ + currOut = output + (globalSampling? batch*numVelocities + velocityIdx : batch)*outputBatchSizeElements; + //SL bwd step + //kAdvectGrid3DSemiLangrange<, , false>(, ); + //kCorrectGrid3DMacCormack(, revertExtrema); + if(boundaryMode==Sampling::BOUNDARY_BORDER) + kAdvectCorrectGrid3DMacCormack<<>>(currInput, tmp_fwd, tmp_min, tmp_max, currVel, currOut); + else if(boundaryMode==Sampling::BOUNDARY_CLAMP) + kAdvectCorrectGrid3DMacCormack<<>>(currInput, tmp_fwd, tmp_min, tmp_max, currVel, currOut); + else if(boundaryMode==Sampling::BOUNDARY_WRAP) + kAdvectCorrectGrid3DMacCormack<<>>(currInput, tmp_fwd, tmp_min, tmp_max, currVel, currOut); + } + } + } + CUDA_CHECK_RETURN(cudaDeviceSynchronize()); + LOG("End AdvectGridKernelLauncher"); +} + +#define DEFINE_GPU_SPECS(T, C, VEC) \ + template<> \ + void AdvectGridKernel::operator()(const GPUDevice& d, \ + const T* input, const long long int* input_shape, \ + const float* velocity, T* tmp_fwd, T* tmp_min, T* tmp_max, \ + const float timestep, const int32_t order, const Sampling::BoundaryMode boundaryMode, \ + const bool revertExtrema, const int32_t numVelocities, const bool globalSampling, \ + T* output, const long long int* output_shape){ \ + AdvectGridKernelLauncher(d, \ + reinterpret_cast(input), input_shape, \ + reinterpret_cast(velocity), reinterpret_cast(tmp_fwd), reinterpret_cast(tmp_min), reinterpret_cast(tmp_max), \ + timestep, order, boundaryMode, \ + revertExtrema, numVelocities, globalSampling, \ + reinterpret_cast(output), output_shape); \ + } \ + template struct AdvectGridKernel; +DEFINE_GPU_SPECS(float, 1, float1); +DEFINE_GPU_SPECS(float, 2, float2); +DEFINE_GPU_SPECS(float, 4, float4); + +/* +template +AdvectGridGradsKernelLauncher(d,){ + + //setup constants + + //zero gradient buffers + + //partially redo fwd step + //SL fwd step for min/max + if(order==2 && revertExtrema){ + kAdvectGrid3DSemiLangrange<, , true>(, ); + } + + if(order==2){ + + kCorrectGrid3DMacCormackGradients(out_grads -> , revertExtrema); + //SL bwd step + kScatter3DGradients(bwd_grads -> fwd_grads); + } + + + kScatter3DGradients(fwd_grads -> input_grads); + +} +*/ + +#endif diff --git a/phitest/render/cuda/src/advect.hpp b/phitest/render/cuda/src/advect.hpp new file mode 100644 index 0000000..e79b9e4 --- /dev/null +++ b/phitest/render/cuda/src/advect.hpp @@ -0,0 +1,25 @@ +#pragma once + +#ifndef _INCLUDE_ADVECT +#define _INCLUDE_ADVECT + +#include "tensorflow/core/framework/tensor_types.h" +#include "tensorflow/core/platform/types.h" +#include"sampling_settings_v2.hpp" + +using GPUDevice = Eigen::GpuDevice; + +template +struct AdvectGridKernel{ + void operator()(const GPUDevice& d, + const T* input, const long long int* input_shape, + const float* velocity, T* tmp_fwd, T* tmp_min, T* tmp_max, + const float timestep, const int32_t order, const Sampling::BoundaryMode boundaryMode, + const bool revertExtrema, const int32_t numVelocities, const bool globalSampling, + T* output, const long long int* output_shape); + + +}; + + +#endif //_INCLUDE_ADVECT diff --git a/phitest/render/cuda/src/blending.hpp b/phitest/render/cuda/src/blending.hpp new file mode 100644 index 0000000..c47b01e --- /dev/null +++ b/phitest/render/cuda/src/blending.hpp @@ -0,0 +1,248 @@ +#pragma once + +#ifndef _INCLUDE_BLENDING +#define _INCLUDE_BLENDING + +#include "blending_settings.hpp" + +namespace Blending{ +template +struct BlendState{ + //returns updated accumulator values + __device__ static inline T_DATA blend(const T_DATA accumulator, const T_DATA cell_in){ + return accumulator + cell_in; + } + //returns grads_cell and updates grads_out to hold grads_in + __device__ static inline T_DATA blendGradients(T_DATA& grads_out, const T_DATA cell_in, T_DATA& cell_out){ + return grads_out; + } +}; + +/* +template +struct BlendState{ + __device__ static inline float1 blend(const float1 acc, const float1 cell_in){ + return acc + cell_in; + } + __device__ static inline float1 blendGradients(float1& grads_out, const float1 cell_in, float1& cell_out){ + return grads_out; + } +};*/ + +template<> +struct BlendState{ + __device__ static inline float4 blend(const float4 acc, const float4 cell_in){ + const float d = max(acc.w + cell_in.w, 0.f); + const float t = exp(-d); //transmissivity + return make_float4(acc.x + t*cell_in.x, acc.y + t*cell_in.y, acc.z + t*cell_in.z, //color + d); //opacity + } + __device__ static inline float4 blendGradients(float4& grads_out, const float4 cell_in, float4& cell_out){ + const float e = exp(-cell_out.w); + cell_out.w = max(cell_out.w - cell_in.w, 0.f); + const float3 cell_light = make_float3(cell_in);//glm::xyz(cell); + const float3 grads_light = make_float3(grads_out);//glm::xyz(grads_out); + + const float d_dacc = grads_out.w - e*vmath::sum(cell_light*grads_light); + const float d_d = d_dacc;//grads_out.w + e*vmath::sum(cell_light*grads_light); + + const float3 d_lacc = grads_light; + const float3 d_l = e*grads_light; + + grads_out = make_float4(d_lacc, d_dacc); + return make_float4(d_l, d_d); + } +}; +template<> +struct BlendState{ + __device__ static inline float2 blend(const float2 acc, const float2 cell_in){ + const float d = max(acc.y + cell_in.y, 0.f); + const float t = exp(-d); //transmissivity + return make_float2(acc.x + t*cell_in.x, //color + d); //opacity + } + __device__ static inline float2 blendGradients(float2& grads_out, const float2 cell_in, float2& cell_out){ + const float e = exp(-cell_out.y); + cell_out.y = max(cell_out.y - cell_in.y, 0.f); + const float cell_light = cell_in.x;//glm::xyz(cell); + const float grads_light = grads_out.x;//glm::xyz(grads_out); + + const float d_dacc = grads_out.y - e*cell_light*grads_light; + const float d_d = d_dacc;//grads_out.y + e*vmath::sum(cell_light*grads_light); + + const float d_lacc = grads_light; + const float d_l = e*grads_light; + + grads_out = make_float2(d_lacc, d_dacc); + return make_float2(d_l, d_d); + } +}; +template struct BlendState; + +//Beer-Lambert without cell self-attenuation +template<> +struct BlendState{ + __device__ static inline float4 blend(const float4 acc, const float4 cell_in){ + const float t = exp(-acc.w); //transmissivity + return make_float4(acc.x + t*cell_in.x, acc.y + t*cell_in.y, acc.z + t*cell_in.z, //color + acc.w + cell_in.w); //opacity + } + __device__ static inline float4 blendGradients(float4& grads_out, const float4 cell_in, float4& cell_out){ + cell_out.w = max(cell_out.w - cell_in.w, 0.f); + const float e = exp(-cell_out.w); + const float3 cell_light = make_float3(cell_in);//glm::xyz(cell); + const float3 grads_light = make_float3(grads_out);//glm::xyz(grads_out); + + const float d_dacc = grads_out.w - e*vmath::sum(cell_light*grads_light); + const float d_d = grads_out.w;// + vmath::sum(e*cell_light*grads_light); + + const float3 d_lacc = grads_light; + const float3 d_l = e*grads_light; + + grads_out = make_float4(d_lacc, d_dacc); + return make_float4(d_l, d_d); + } +}; +template<> +struct BlendState{ + __device__ static inline float2 blend(const float2 acc, const float2 cell_in){ + const float t = exp(-acc.y); //transmissivity + return make_float2(acc.x + t*cell_in.x, //color + acc.y + cell_in.y); //opacity + } + __device__ static inline float2 blendGradients(float2& grads_out, const float2 cell_in, float2& cell_out){ + cell_out.y = max(cell_out.y - cell_in.y, 0.f); + const float e = exp(-cell_out.y); + const float cell_light = cell_in.x;//glm::xyz(cell); + const float grads_light = grads_out.x;//glm::xyz(grads_out); + + const float d_dacc = grads_out.y - e*cell_light*grads_light; + const float d_d = grads_out.y;// + vmath::sum(e*cell_light*grads_light); + + const float d_lacc = grads_light; + const float d_l = e*grads_light; + + grads_out = make_float2(d_lacc, d_dacc); + return make_float2(d_l, d_d); + } +}; +template struct BlendState; + +template<> +struct BlendState{ + __device__ static inline float4 blend(const float4 acc, const float4 cell_in){ + const float t = (1.f-acc.w); //transmissivity + return make_float4(acc.x + t*cell_in.x, acc.y + t*cell_in.y, acc.z + t*cell_in.z, //vmath::lerp(lm::rgb(cell)*cell.a, glm::rgb(acc),acc.a), + //acc.a + (1.f-acc.a)*cell.a); //accumulatedDensity*accumulatedLight + (1 - accumulatedDensity)*cellLight; + acc.w + t*cell_in.w); //1.f - (1.f - acc.w) * (1.f - cell_in.w)); + } + __device__ static inline float4 blendGradients(float4& grads_out, const float4 cell_in, float4& cell_out){ + cell_out.w = (1.f-cell_out.w)/(1.f-cell_in.w); + const float t = (1.f-cell_out.w); //transmissivity + const float3 cell_light = make_float3(cell_in); + const float3 grads_light = make_float3(grads_out); + + const float d_dacc = (1 - cell_in.w)*grads_out.w - vmath::sum(cell_light*grads_light); + const float d_d = t*grads_out.w; + + const float3 d_lacc = grads_light; + const float3 d_l = t*grads_light; + + grads_out = make_float4(d_lacc, d_dacc); + return make_float4(d_l, d_d); + } +}; +template<> +struct BlendState{ + __device__ static inline float2 blend(const float2 acc, const float2 cell_in){ + const float t = (1.f-acc.y); //transmissivity + return make_float2(acc.x + t*cell_in.x, //vmath::lerp(lm::rgb(cell)*cell.a, glm::rgb(acc),acc.a), + //acc.a + (1.f-acc.a)*cell.a); //accumulatedDensity*accumulatedLight + (1 - accumulatedDensity)*cellLight; + acc.y + t*cell_in.y); //1.f - (1.f - acc.w) * (1.f - cell_in.w)); + } + __device__ static inline float2 blendGradients(float2& grads_out, const float2 cell_in, float2& cell_out){ + cell_out.y = (1.f-cell_out.y)/(1.f-cell_in.y); + const float t = (1.f-cell_out.y); //transmissivity + const float cell_light = cell_in.x; + const float grads_light = grads_out.x; + + const float d_dacc = (1 - cell_in.y)*grads_out.y - cell_light*grads_light; + const float d_d = t*grads_out.y; + + const float d_lacc = grads_light; + const float d_l = t*grads_light; + + grads_out = make_float2(d_lacc, d_dacc); + return make_float2(d_l, d_d); + } +}; +template struct BlendState; + +template<> +struct BlendState{ + __device__ static inline float4 blend(const float4 acc, const float4 cell_in){ + const float t = (1.f-cell_in.w); //transmissivity + return make_float4(acc.x + t*cell_in.x, acc.y + t*cell_in.y, acc.z + t*cell_in.z, //vmath::lerp(lm::rgb(cell)*cell.a, glm::rgb(acc),acc.a), + acc.w + cell_in.w); //unused + } + __device__ static inline float4 blendGradients(float4& grads_out, const float4 cell_in, float4& cell_out){ + cell_out.w = cell_out.w-cell_in.w; + const float t = (1.f-cell_in.w); //transmissivity + const float3 cell_light = make_float3(cell_in); + const float3 grads_light = make_float3(grads_out); + + const float d_dacc = grads_out.w; + const float d_d = grads_out.w - vmath::sum(cell_light*grads_light) ; + + const float3 d_lacc = grads_light; + const float3 d_l = t*grads_light; + + grads_out = make_float4(d_lacc, d_dacc); + return make_float4(d_l, d_d); + } +}; +template<> +struct BlendState{ + __device__ static inline float2 blend(const float2 acc, const float2 cell_in){ + const float t = (1.f-cell_in.y); //transmissivity + return make_float2(acc.x + t*cell_in.x, //vmath::lerp(lm::rgb(cell)*cell.a, glm::rgb(acc),acc.a), + acc.y + cell_in.y); //unused + } + __device__ static inline float2 blendGradients(float2& grads_out, const float2 cell_in, float2& cell_out){ + cell_out.y = cell_out.y-cell_in.y; + const float t = (1.f-cell_in.y); //transmissivity + const float cell_light = cell_in.x; + const float grads_light = grads_out.x; + + const float d_dacc = grads_out.y; + const float d_d = grads_out.y - cell_light*grads_light; + + const float d_lacc = grads_light; + const float d_l = t*grads_light; + + grads_out = make_float2(d_lacc, d_dacc); + return make_float2(d_l, d_d); + } +}; +template struct BlendState; + + +template struct BlendState; +template struct BlendState; +template struct BlendState; +/* +template +struct BlendState{ + __device__ static inline T blend(const T acc, const T cell_in){ + return acc + cell_in; + } + __device__ static inline T blendGradients(T& grads_out, const T cell_in, T& cell_out){ + return grads_out; + } +};*/ +} + + + + +#endif //_INCLUDE_BLENDING \ No newline at end of file diff --git a/phitest/render/cuda/src/blending_settings.hpp b/phitest/render/cuda/src/blending_settings.hpp new file mode 100644 index 0000000..9948c77 --- /dev/null +++ b/phitest/render/cuda/src/blending_settings.hpp @@ -0,0 +1,15 @@ +#pragma once + +#ifndef _INCLUDE_BLENDINGSETTINGS +#define _INCLUDE_BLENDINGSETTINGS +namespace Blending{ +enum BlendMode{ + BLEND_BEERLAMBERT, + BLEND_BEERLAMBERT_EXCLUSIVE, + BLEND_ALPHA, + BLEND_ALPHAADDITIVE, + BLEND_ADDITIVE, +}; +} + +#endif //_INCLUDE_BLENDINGSETTINGS \ No newline at end of file diff --git a/phitest/render/cuda/src/bounds_checks.hpp b/phitest/render/cuda/src/bounds_checks.hpp new file mode 100644 index 0000000..3dbfcc8 --- /dev/null +++ b/phitest/render/cuda/src/bounds_checks.hpp @@ -0,0 +1,45 @@ + +#pragma once + +#ifndef _INCLUDE_BOUNDSCHECKS +#define _INCLUDE_BOUNDSCHECKS + +//for bounds checking +#define CHECK_BOUNDS_SV3S(l, c1, v, c2, u) l c1 v.x && v.x c2 u && l c1 v.y && v.y c2 u && l c1 v.z && v.z c2 u +#define CHECK_BOUNDS_SV3V3(l, c1, v, c2, u) l c1 v.x && v.x c2 u.x && l c1 v.y && v.y c2 u.y && l c1 v.z && v.z c2 u.z +#define CHECK_BOUNDS_V3V3V3(l, c1, v, c2, u) l.x c1 v.x && v.x c2 u.x && l.x c1 v.y && v.y c2 u.y && l.x c1 v.z && v.z c2 u.z +#define CHECK_BOUND_SV3(v1, c, v2) v1 c v2.x && v1 c v2.y && v1 c v2.z +#define CHECK_BOUND_V3S(v1, c, v2) v1.x c v2 && v1.y c v2 && v1.z c v2 +#define CHECK_BOUND_V3V3(v1, c, v2) v1.x c v2.x && v1.y c v2.y && v1 c v2.z +/* +template +__device__ inline bool isInDimensions(const T position, const D dimensions){ + return (position.x < dimensions.x && position.y < dimensions.y && position.z < dimensions.z); +} +template +__device__ inline bool isInDimensions(const T x, const T y, const T z, const D dimensions){ + return (x < dimensions.x && y < dimensions.y && z < dimensions.z); +} +template +__device__ inline bool isNonNegative(const V3 position){ + //return (position.x >=0 && position.y >=0 && position.z >=0); + return CHECK_BOUND_SV3(0, <=, position); +} +__device__ inline bool isNonNegative(const glm::vec3 position){ + //return (position.x >=0 && position.y >=0 && position.z >=0); + return CHECK_BOUND_SV3(0.f, <=, position); +} +*/ +/* +inline __device__ __host__ bool checkBounds3D(const int3 idx, const int3 dims){ + return CHECK_BOUNDS_SV3V3(0, <=, idx, <, dims); +} +inline __device__ __host__ bool checkBounds3D(const float3 idx, const int3 dims){ + return CHECK_BOUNDS_SV3V3(0, <=, idx, <, dims); +} +inline __device__ __host__ bool checkUpperBounds3D(const int3 idx, const int3 dims){ + return CHECK_BOUNDS_V3V3(idx, <, dims); +} +*/ + +#endif //_INCLUDE_BOUNDSCHECKS \ No newline at end of file diff --git a/phitest/render/cuda/src/cuda-samples/Common/helper_cuda.h b/phitest/render/cuda/src/cuda-samples/Common/helper_cuda.h new file mode 100644 index 0000000..2ac4536 --- /dev/null +++ b/phitest/render/cuda/src/cuda-samples/Common/helper_cuda.h @@ -0,0 +1,967 @@ +/* Copyright (c) 2019, NVIDIA CORPORATION. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of NVIDIA CORPORATION nor the names of its + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +//////////////////////////////////////////////////////////////////////////////// +// These are CUDA Helper functions for initialization and error checking + +#ifndef COMMON_HELPER_CUDA_H_ +#define COMMON_HELPER_CUDA_H_ + +#pragma once + +#include +#include +#include +#include + +#include "helper_string.h" + +#ifndef EXIT_WAIVED +#define EXIT_WAIVED 2 +#endif + +// Note, it is required that your SDK sample to include the proper header +// files, please refer the CUDA examples for examples of the needed CUDA +// headers, which may change depending on which CUDA functions are used. + +// CUDA Runtime error messages +#ifdef __DRIVER_TYPES_H__ +static const char *_cudaGetErrorEnum(cudaError_t error) { + return cudaGetErrorName(error); +} +#endif + +#ifdef CUDA_DRIVER_API +// CUDA Driver API errors +static const char *_cudaGetErrorEnum(CUresult error) { + static char unknown[] = ""; + const char *ret = NULL; + cuGetErrorName(error, &ret); + return ret ? ret : unknown; +} +#endif + +#ifdef CUBLAS_API_H_ +// cuBLAS API errors +static const char *_cudaGetErrorEnum(cublasStatus_t error) { + switch (error) { + case CUBLAS_STATUS_SUCCESS: + return "CUBLAS_STATUS_SUCCESS"; + + case CUBLAS_STATUS_NOT_INITIALIZED: + return "CUBLAS_STATUS_NOT_INITIALIZED"; + + case CUBLAS_STATUS_ALLOC_FAILED: + return "CUBLAS_STATUS_ALLOC_FAILED"; + + case CUBLAS_STATUS_INVALID_VALUE: + return "CUBLAS_STATUS_INVALID_VALUE"; + + case CUBLAS_STATUS_ARCH_MISMATCH: + return "CUBLAS_STATUS_ARCH_MISMATCH"; + + case CUBLAS_STATUS_MAPPING_ERROR: + return "CUBLAS_STATUS_MAPPING_ERROR"; + + case CUBLAS_STATUS_EXECUTION_FAILED: + return "CUBLAS_STATUS_EXECUTION_FAILED"; + + case CUBLAS_STATUS_INTERNAL_ERROR: + return "CUBLAS_STATUS_INTERNAL_ERROR"; + + case CUBLAS_STATUS_NOT_SUPPORTED: + return "CUBLAS_STATUS_NOT_SUPPORTED"; + + case CUBLAS_STATUS_LICENSE_ERROR: + return "CUBLAS_STATUS_LICENSE_ERROR"; + } + + return ""; +} +#endif + +#ifdef _CUFFT_H_ +// cuFFT API errors +static const char *_cudaGetErrorEnum(cufftResult error) { + switch (error) { + case CUFFT_SUCCESS: + return "CUFFT_SUCCESS"; + + case CUFFT_INVALID_PLAN: + return "CUFFT_INVALID_PLAN"; + + case CUFFT_ALLOC_FAILED: + return "CUFFT_ALLOC_FAILED"; + + case CUFFT_INVALID_TYPE: + return "CUFFT_INVALID_TYPE"; + + case CUFFT_INVALID_VALUE: + return "CUFFT_INVALID_VALUE"; + + case CUFFT_INTERNAL_ERROR: + return "CUFFT_INTERNAL_ERROR"; + + case CUFFT_EXEC_FAILED: + return "CUFFT_EXEC_FAILED"; + + case CUFFT_SETUP_FAILED: + return "CUFFT_SETUP_FAILED"; + + case CUFFT_INVALID_SIZE: + return "CUFFT_INVALID_SIZE"; + + case CUFFT_UNALIGNED_DATA: + return "CUFFT_UNALIGNED_DATA"; + + case CUFFT_INCOMPLETE_PARAMETER_LIST: + return "CUFFT_INCOMPLETE_PARAMETER_LIST"; + + case CUFFT_INVALID_DEVICE: + return "CUFFT_INVALID_DEVICE"; + + case CUFFT_PARSE_ERROR: + return "CUFFT_PARSE_ERROR"; + + case CUFFT_NO_WORKSPACE: + return "CUFFT_NO_WORKSPACE"; + + case CUFFT_NOT_IMPLEMENTED: + return "CUFFT_NOT_IMPLEMENTED"; + + case CUFFT_LICENSE_ERROR: + return "CUFFT_LICENSE_ERROR"; + + case CUFFT_NOT_SUPPORTED: + return "CUFFT_NOT_SUPPORTED"; + } + + return ""; +} +#endif + +#ifdef CUSPARSEAPI +// cuSPARSE API errors +static const char *_cudaGetErrorEnum(cusparseStatus_t error) { + switch (error) { + case CUSPARSE_STATUS_SUCCESS: + return "CUSPARSE_STATUS_SUCCESS"; + + case CUSPARSE_STATUS_NOT_INITIALIZED: + return "CUSPARSE_STATUS_NOT_INITIALIZED"; + + case CUSPARSE_STATUS_ALLOC_FAILED: + return "CUSPARSE_STATUS_ALLOC_FAILED"; + + case CUSPARSE_STATUS_INVALID_VALUE: + return "CUSPARSE_STATUS_INVALID_VALUE"; + + case CUSPARSE_STATUS_ARCH_MISMATCH: + return "CUSPARSE_STATUS_ARCH_MISMATCH"; + + case CUSPARSE_STATUS_MAPPING_ERROR: + return "CUSPARSE_STATUS_MAPPING_ERROR"; + + case CUSPARSE_STATUS_EXECUTION_FAILED: + return "CUSPARSE_STATUS_EXECUTION_FAILED"; + + case CUSPARSE_STATUS_INTERNAL_ERROR: + return "CUSPARSE_STATUS_INTERNAL_ERROR"; + + case CUSPARSE_STATUS_MATRIX_TYPE_NOT_SUPPORTED: + return "CUSPARSE_STATUS_MATRIX_TYPE_NOT_SUPPORTED"; + } + + return ""; +} +#endif + +#ifdef CUSOLVER_COMMON_H_ +// cuSOLVER API errors +static const char *_cudaGetErrorEnum(cusolverStatus_t error) { + switch (error) { + case CUSOLVER_STATUS_SUCCESS: + return "CUSOLVER_STATUS_SUCCESS"; + case CUSOLVER_STATUS_NOT_INITIALIZED: + return "CUSOLVER_STATUS_NOT_INITIALIZED"; + case CUSOLVER_STATUS_ALLOC_FAILED: + return "CUSOLVER_STATUS_ALLOC_FAILED"; + case CUSOLVER_STATUS_INVALID_VALUE: + return "CUSOLVER_STATUS_INVALID_VALUE"; + case CUSOLVER_STATUS_ARCH_MISMATCH: + return "CUSOLVER_STATUS_ARCH_MISMATCH"; + case CUSOLVER_STATUS_MAPPING_ERROR: + return "CUSOLVER_STATUS_MAPPING_ERROR"; + case CUSOLVER_STATUS_EXECUTION_FAILED: + return "CUSOLVER_STATUS_EXECUTION_FAILED"; + case CUSOLVER_STATUS_INTERNAL_ERROR: + return "CUSOLVER_STATUS_INTERNAL_ERROR"; + case CUSOLVER_STATUS_MATRIX_TYPE_NOT_SUPPORTED: + return "CUSOLVER_STATUS_MATRIX_TYPE_NOT_SUPPORTED"; + case CUSOLVER_STATUS_NOT_SUPPORTED: + return "CUSOLVER_STATUS_NOT_SUPPORTED "; + case CUSOLVER_STATUS_ZERO_PIVOT: + return "CUSOLVER_STATUS_ZERO_PIVOT"; + case CUSOLVER_STATUS_INVALID_LICENSE: + return "CUSOLVER_STATUS_INVALID_LICENSE"; + } + + return ""; +} +#endif + +#ifdef CURAND_H_ +// cuRAND API errors +static const char *_cudaGetErrorEnum(curandStatus_t error) { + switch (error) { + case CURAND_STATUS_SUCCESS: + return "CURAND_STATUS_SUCCESS"; + + case CURAND_STATUS_VERSION_MISMATCH: + return "CURAND_STATUS_VERSION_MISMATCH"; + + case CURAND_STATUS_NOT_INITIALIZED: + return "CURAND_STATUS_NOT_INITIALIZED"; + + case CURAND_STATUS_ALLOCATION_FAILED: + return "CURAND_STATUS_ALLOCATION_FAILED"; + + case CURAND_STATUS_TYPE_ERROR: + return "CURAND_STATUS_TYPE_ERROR"; + + case CURAND_STATUS_OUT_OF_RANGE: + return "CURAND_STATUS_OUT_OF_RANGE"; + + case CURAND_STATUS_LENGTH_NOT_MULTIPLE: + return "CURAND_STATUS_LENGTH_NOT_MULTIPLE"; + + case CURAND_STATUS_DOUBLE_PRECISION_REQUIRED: + return "CURAND_STATUS_DOUBLE_PRECISION_REQUIRED"; + + case CURAND_STATUS_LAUNCH_FAILURE: + return "CURAND_STATUS_LAUNCH_FAILURE"; + + case CURAND_STATUS_PREEXISTING_FAILURE: + return "CURAND_STATUS_PREEXISTING_FAILURE"; + + case CURAND_STATUS_INITIALIZATION_FAILED: + return "CURAND_STATUS_INITIALIZATION_FAILED"; + + case CURAND_STATUS_ARCH_MISMATCH: + return "CURAND_STATUS_ARCH_MISMATCH"; + + case CURAND_STATUS_INTERNAL_ERROR: + return "CURAND_STATUS_INTERNAL_ERROR"; + } + + return ""; +} +#endif + +#ifdef NVJPEGAPI +// nvJPEG API errors +static const char *_cudaGetErrorEnum(nvjpegStatus_t error) { + switch (error) { + case NVJPEG_STATUS_SUCCESS: + return "NVJPEG_STATUS_SUCCESS"; + + case NVJPEG_STATUS_NOT_INITIALIZED: + return "NVJPEG_STATUS_NOT_INITIALIZED"; + + case NVJPEG_STATUS_INVALID_PARAMETER: + return "NVJPEG_STATUS_INVALID_PARAMETER"; + + case NVJPEG_STATUS_BAD_JPEG: + return "NVJPEG_STATUS_BAD_JPEG"; + + case NVJPEG_STATUS_JPEG_NOT_SUPPORTED: + return "NVJPEG_STATUS_JPEG_NOT_SUPPORTED"; + + case NVJPEG_STATUS_ALLOCATOR_FAILURE: + return "NVJPEG_STATUS_ALLOCATOR_FAILURE"; + + case NVJPEG_STATUS_EXECUTION_FAILED: + return "NVJPEG_STATUS_EXECUTION_FAILED"; + + case NVJPEG_STATUS_ARCH_MISMATCH: + return "NVJPEG_STATUS_ARCH_MISMATCH"; + + case NVJPEG_STATUS_INTERNAL_ERROR: + return "NVJPEG_STATUS_INTERNAL_ERROR"; + } + + return ""; +} +#endif + +#ifdef NV_NPPIDEFS_H +// NPP API errors +static const char *_cudaGetErrorEnum(NppStatus error) { + switch (error) { + case NPP_NOT_SUPPORTED_MODE_ERROR: + return "NPP_NOT_SUPPORTED_MODE_ERROR"; + + case NPP_ROUND_MODE_NOT_SUPPORTED_ERROR: + return "NPP_ROUND_MODE_NOT_SUPPORTED_ERROR"; + + case NPP_RESIZE_NO_OPERATION_ERROR: + return "NPP_RESIZE_NO_OPERATION_ERROR"; + + case NPP_NOT_SUFFICIENT_COMPUTE_CAPABILITY: + return "NPP_NOT_SUFFICIENT_COMPUTE_CAPABILITY"; + +#if ((NPP_VERSION_MAJOR << 12) + (NPP_VERSION_MINOR << 4)) <= 0x5000 + + case NPP_BAD_ARG_ERROR: + return "NPP_BAD_ARGUMENT_ERROR"; + + case NPP_COEFF_ERROR: + return "NPP_COEFFICIENT_ERROR"; + + case NPP_RECT_ERROR: + return "NPP_RECTANGLE_ERROR"; + + case NPP_QUAD_ERROR: + return "NPP_QUADRANGLE_ERROR"; + + case NPP_MEM_ALLOC_ERR: + return "NPP_MEMORY_ALLOCATION_ERROR"; + + case NPP_HISTO_NUMBER_OF_LEVELS_ERROR: + return "NPP_HISTOGRAM_NUMBER_OF_LEVELS_ERROR"; + + case NPP_INVALID_INPUT: + return "NPP_INVALID_INPUT"; + + case NPP_POINTER_ERROR: + return "NPP_POINTER_ERROR"; + + case NPP_WARNING: + return "NPP_WARNING"; + + case NPP_ODD_ROI_WARNING: + return "NPP_ODD_ROI_WARNING"; +#else + + // These are for CUDA 5.5 or higher + case NPP_BAD_ARGUMENT_ERROR: + return "NPP_BAD_ARGUMENT_ERROR"; + + case NPP_COEFFICIENT_ERROR: + return "NPP_COEFFICIENT_ERROR"; + + case NPP_RECTANGLE_ERROR: + return "NPP_RECTANGLE_ERROR"; + + case NPP_QUADRANGLE_ERROR: + return "NPP_QUADRANGLE_ERROR"; + + case NPP_MEMORY_ALLOCATION_ERR: + return "NPP_MEMORY_ALLOCATION_ERROR"; + + case NPP_HISTOGRAM_NUMBER_OF_LEVELS_ERROR: + return "NPP_HISTOGRAM_NUMBER_OF_LEVELS_ERROR"; + + case NPP_INVALID_HOST_POINTER_ERROR: + return "NPP_INVALID_HOST_POINTER_ERROR"; + + case NPP_INVALID_DEVICE_POINTER_ERROR: + return "NPP_INVALID_DEVICE_POINTER_ERROR"; +#endif + + case NPP_LUT_NUMBER_OF_LEVELS_ERROR: + return "NPP_LUT_NUMBER_OF_LEVELS_ERROR"; + + case NPP_TEXTURE_BIND_ERROR: + return "NPP_TEXTURE_BIND_ERROR"; + + case NPP_WRONG_INTERSECTION_ROI_ERROR: + return "NPP_WRONG_INTERSECTION_ROI_ERROR"; + + case NPP_NOT_EVEN_STEP_ERROR: + return "NPP_NOT_EVEN_STEP_ERROR"; + + case NPP_INTERPOLATION_ERROR: + return "NPP_INTERPOLATION_ERROR"; + + case NPP_RESIZE_FACTOR_ERROR: + return "NPP_RESIZE_FACTOR_ERROR"; + + case NPP_HAAR_CLASSIFIER_PIXEL_MATCH_ERROR: + return "NPP_HAAR_CLASSIFIER_PIXEL_MATCH_ERROR"; + +#if ((NPP_VERSION_MAJOR << 12) + (NPP_VERSION_MINOR << 4)) <= 0x5000 + + case NPP_MEMFREE_ERR: + return "NPP_MEMFREE_ERR"; + + case NPP_MEMSET_ERR: + return "NPP_MEMSET_ERR"; + + case NPP_MEMCPY_ERR: + return "NPP_MEMCPY_ERROR"; + + case NPP_MIRROR_FLIP_ERR: + return "NPP_MIRROR_FLIP_ERR"; +#else + + case NPP_MEMFREE_ERROR: + return "NPP_MEMFREE_ERROR"; + + case NPP_MEMSET_ERROR: + return "NPP_MEMSET_ERROR"; + + case NPP_MEMCPY_ERROR: + return "NPP_MEMCPY_ERROR"; + + case NPP_MIRROR_FLIP_ERROR: + return "NPP_MIRROR_FLIP_ERROR"; +#endif + + case NPP_ALIGNMENT_ERROR: + return "NPP_ALIGNMENT_ERROR"; + + case NPP_STEP_ERROR: + return "NPP_STEP_ERROR"; + + case NPP_SIZE_ERROR: + return "NPP_SIZE_ERROR"; + + case NPP_NULL_POINTER_ERROR: + return "NPP_NULL_POINTER_ERROR"; + + case NPP_CUDA_KERNEL_EXECUTION_ERROR: + return "NPP_CUDA_KERNEL_EXECUTION_ERROR"; + + case NPP_NOT_IMPLEMENTED_ERROR: + return "NPP_NOT_IMPLEMENTED_ERROR"; + + case NPP_ERROR: + return "NPP_ERROR"; + + case NPP_SUCCESS: + return "NPP_SUCCESS"; + + case NPP_WRONG_INTERSECTION_QUAD_WARNING: + return "NPP_WRONG_INTERSECTION_QUAD_WARNING"; + + case NPP_MISALIGNED_DST_ROI_WARNING: + return "NPP_MISALIGNED_DST_ROI_WARNING"; + + case NPP_AFFINE_QUAD_INCORRECT_WARNING: + return "NPP_AFFINE_QUAD_INCORRECT_WARNING"; + + case NPP_DOUBLE_SIZE_WARNING: + return "NPP_DOUBLE_SIZE_WARNING"; + + case NPP_WRONG_INTERSECTION_ROI_WARNING: + return "NPP_WRONG_INTERSECTION_ROI_WARNING"; + +#if ((NPP_VERSION_MAJOR << 12) + (NPP_VERSION_MINOR << 4)) >= 0x6000 + /* These are 6.0 or higher */ + case NPP_LUT_PALETTE_BITSIZE_ERROR: + return "NPP_LUT_PALETTE_BITSIZE_ERROR"; + + case NPP_ZC_MODE_NOT_SUPPORTED_ERROR: + return "NPP_ZC_MODE_NOT_SUPPORTED_ERROR"; + + case NPP_QUALITY_INDEX_ERROR: + return "NPP_QUALITY_INDEX_ERROR"; + + case NPP_CHANNEL_ORDER_ERROR: + return "NPP_CHANNEL_ORDER_ERROR"; + + case NPP_ZERO_MASK_VALUE_ERROR: + return "NPP_ZERO_MASK_VALUE_ERROR"; + + case NPP_NUMBER_OF_CHANNELS_ERROR: + return "NPP_NUMBER_OF_CHANNELS_ERROR"; + + case NPP_COI_ERROR: + return "NPP_COI_ERROR"; + + case NPP_DIVISOR_ERROR: + return "NPP_DIVISOR_ERROR"; + + case NPP_CHANNEL_ERROR: + return "NPP_CHANNEL_ERROR"; + + case NPP_STRIDE_ERROR: + return "NPP_STRIDE_ERROR"; + + case NPP_ANCHOR_ERROR: + return "NPP_ANCHOR_ERROR"; + + case NPP_MASK_SIZE_ERROR: + return "NPP_MASK_SIZE_ERROR"; + + case NPP_MOMENT_00_ZERO_ERROR: + return "NPP_MOMENT_00_ZERO_ERROR"; + + case NPP_THRESHOLD_NEGATIVE_LEVEL_ERROR: + return "NPP_THRESHOLD_NEGATIVE_LEVEL_ERROR"; + + case NPP_THRESHOLD_ERROR: + return "NPP_THRESHOLD_ERROR"; + + case NPP_CONTEXT_MATCH_ERROR: + return "NPP_CONTEXT_MATCH_ERROR"; + + case NPP_FFT_FLAG_ERROR: + return "NPP_FFT_FLAG_ERROR"; + + case NPP_FFT_ORDER_ERROR: + return "NPP_FFT_ORDER_ERROR"; + + case NPP_SCALE_RANGE_ERROR: + return "NPP_SCALE_RANGE_ERROR"; + + case NPP_DATA_TYPE_ERROR: + return "NPP_DATA_TYPE_ERROR"; + + case NPP_OUT_OFF_RANGE_ERROR: + return "NPP_OUT_OFF_RANGE_ERROR"; + + case NPP_DIVIDE_BY_ZERO_ERROR: + return "NPP_DIVIDE_BY_ZERO_ERROR"; + + case NPP_RANGE_ERROR: + return "NPP_RANGE_ERROR"; + + case NPP_NO_MEMORY_ERROR: + return "NPP_NO_MEMORY_ERROR"; + + case NPP_ERROR_RESERVED: + return "NPP_ERROR_RESERVED"; + + case NPP_NO_OPERATION_WARNING: + return "NPP_NO_OPERATION_WARNING"; + + case NPP_DIVIDE_BY_ZERO_WARNING: + return "NPP_DIVIDE_BY_ZERO_WARNING"; +#endif + +#if ((NPP_VERSION_MAJOR << 12) + (NPP_VERSION_MINOR << 4)) >= 0x7000 + /* These are 7.0 or higher */ + case NPP_OVERFLOW_ERROR: + return "NPP_OVERFLOW_ERROR"; + + case NPP_CORRUPTED_DATA_ERROR: + return "NPP_CORRUPTED_DATA_ERROR"; +#endif + } + + return ""; +} +#endif + +template +void check(T result, char const *const func, const char *const file, + int const line) { + if (result) { + fprintf(stderr, "CUDA error at %s:%d code=%d(%s) \"%s\" \n", file, line, + static_cast(result), _cudaGetErrorEnum(result), func); + exit(EXIT_FAILURE); + } +} + +#ifdef __DRIVER_TYPES_H__ +// This will output the proper CUDA error strings in the event +// that a CUDA host call returns an error +#define checkCudaErrors(val) check((val), #val, __FILE__, __LINE__) + +// This will output the proper error string when calling cudaGetLastError +#define getLastCudaError(msg) __getLastCudaError(msg, __FILE__, __LINE__) + +inline void __getLastCudaError(const char *errorMessage, const char *file, + const int line) { + cudaError_t err = cudaGetLastError(); + + if (cudaSuccess != err) { + fprintf(stderr, + "%s(%i) : getLastCudaError() CUDA error :" + " %s : (%d) %s.\n", + file, line, errorMessage, static_cast(err), + cudaGetErrorString(err)); + exit(EXIT_FAILURE); + } +} + +// This will only print the proper error string when calling cudaGetLastError +// but not exit program incase error detected. +#define printLastCudaError(msg) __printLastCudaError(msg, __FILE__, __LINE__) + +inline void __printLastCudaError(const char *errorMessage, const char *file, + const int line) { + cudaError_t err = cudaGetLastError(); + + if (cudaSuccess != err) { + fprintf(stderr, + "%s(%i) : getLastCudaError() CUDA error :" + " %s : (%d) %s.\n", + file, line, errorMessage, static_cast(err), + cudaGetErrorString(err)); + } +} +#endif + +#ifndef MAX +#define MAX(a, b) (a > b ? a : b) +#endif + +// Float To Int conversion +inline int ftoi(float value) { + return (value >= 0 ? static_cast(value + 0.5) + : static_cast(value - 0.5)); +} + +// Beginning of GPU Architecture definitions +inline int _ConvertSMVer2Cores(int major, int minor) { + // Defines for GPU Architecture types (using the SM version to determine + // the # of cores per SM + typedef struct { + int SM; // 0xMm (hexidecimal notation), M = SM Major version, + // and m = SM minor version + int Cores; + } sSMtoCores; + + sSMtoCores nGpuArchCoresPerSM[] = { + {0x30, 192}, + {0x32, 192}, + {0x35, 192}, + {0x37, 192}, + {0x50, 128}, + {0x52, 128}, + {0x53, 128}, + {0x60, 64}, + {0x61, 128}, + {0x62, 128}, + {0x70, 64}, + {0x72, 64}, + {0x75, 64}, + {0x80, 64}, + {0x86, 128}, + {-1, -1}}; + + int index = 0; + + while (nGpuArchCoresPerSM[index].SM != -1) { + if (nGpuArchCoresPerSM[index].SM == ((major << 4) + minor)) { + return nGpuArchCoresPerSM[index].Cores; + } + + index++; + } + + // If we don't find the values, we default use the previous one + // to run properly + printf( + "MapSMtoCores for SM %d.%d is undefined." + " Default to use %d Cores/SM\n", + major, minor, nGpuArchCoresPerSM[index - 1].Cores); + return nGpuArchCoresPerSM[index - 1].Cores; +} + +inline const char* _ConvertSMVer2ArchName(int major, int minor) { + // Defines for GPU Architecture types (using the SM version to determine + // the GPU Arch name) + typedef struct { + int SM; // 0xMm (hexidecimal notation), M = SM Major version, + // and m = SM minor version + const char* name; + } sSMtoArchName; + + sSMtoArchName nGpuArchNameSM[] = { + {0x30, "Kepler"}, + {0x32, "Kepler"}, + {0x35, "Kepler"}, + {0x37, "Kepler"}, + {0x50, "Maxwell"}, + {0x52, "Maxwell"}, + {0x53, "Maxwell"}, + {0x60, "Pascal"}, + {0x61, "Pascal"}, + {0x62, "Pascal"}, + {0x70, "Volta"}, + {0x72, "Xavier"}, + {0x75, "Turing"}, + {0x80, "Ampere"}, + {0x86, "Ampere"}, + {-1, "Graphics Device"}}; + + int index = 0; + + while (nGpuArchNameSM[index].SM != -1) { + if (nGpuArchNameSM[index].SM == ((major << 4) + minor)) { + return nGpuArchNameSM[index].name; + } + + index++; + } + + // If we don't find the values, we default use the previous one + // to run properly + printf( + "MapSMtoArchName for SM %d.%d is undefined." + " Default to use %s\n", + major, minor, nGpuArchNameSM[index - 1].name); + return nGpuArchNameSM[index - 1].name; +} + // end of GPU Architecture definitions + +#ifdef __CUDA_RUNTIME_H__ +// General GPU Device CUDA Initialization +inline int gpuDeviceInit(int devID) { + int device_count; + checkCudaErrors(cudaGetDeviceCount(&device_count)); + + if (device_count == 0) { + fprintf(stderr, + "gpuDeviceInit() CUDA error: " + "no devices supporting CUDA.\n"); + exit(EXIT_FAILURE); + } + + if (devID < 0) { + devID = 0; + } + + if (devID > device_count - 1) { + fprintf(stderr, "\n"); + fprintf(stderr, ">> %d CUDA capable GPU device(s) detected. <<\n", + device_count); + fprintf(stderr, + ">> gpuDeviceInit (-device=%d) is not a valid" + " GPU device. <<\n", + devID); + fprintf(stderr, "\n"); + return -devID; + } + + int computeMode = -1, major = 0, minor = 0; + checkCudaErrors(cudaDeviceGetAttribute(&computeMode, cudaDevAttrComputeMode, devID)); + checkCudaErrors(cudaDeviceGetAttribute(&major, cudaDevAttrComputeCapabilityMajor, devID)); + checkCudaErrors(cudaDeviceGetAttribute(&minor, cudaDevAttrComputeCapabilityMinor, devID)); + if (computeMode == cudaComputeModeProhibited) { + fprintf(stderr, + "Error: device is running in , no threads can use cudaSetDevice().\n"); + return -1; + } + + if (major < 1) { + fprintf(stderr, "gpuDeviceInit(): GPU device does not support CUDA.\n"); + exit(EXIT_FAILURE); + } + + checkCudaErrors(cudaSetDevice(devID)); + printf("gpuDeviceInit() CUDA Device [%d]: \"%s\n", devID, _ConvertSMVer2ArchName(major, minor)); + + return devID; +} + +// This function returns the best GPU (with maximum GFLOPS) +inline int gpuGetMaxGflopsDeviceId() { + int current_device = 0, sm_per_multiproc = 0; + int max_perf_device = 0; + int device_count = 0; + int devices_prohibited = 0; + + uint64_t max_compute_perf = 0; + checkCudaErrors(cudaGetDeviceCount(&device_count)); + + if (device_count == 0) { + fprintf(stderr, + "gpuGetMaxGflopsDeviceId() CUDA error:" + " no devices supporting CUDA.\n"); + exit(EXIT_FAILURE); + } + + // Find the best CUDA capable GPU device + current_device = 0; + + while (current_device < device_count) { + int computeMode = -1, major = 0, minor = 0; + checkCudaErrors(cudaDeviceGetAttribute(&computeMode, cudaDevAttrComputeMode, current_device)); + checkCudaErrors(cudaDeviceGetAttribute(&major, cudaDevAttrComputeCapabilityMajor, current_device)); + checkCudaErrors(cudaDeviceGetAttribute(&minor, cudaDevAttrComputeCapabilityMinor, current_device)); + + // If this GPU is not running on Compute Mode prohibited, + // then we can add it to the list + if (computeMode != cudaComputeModeProhibited) { + if (major == 9999 && minor == 9999) { + sm_per_multiproc = 1; + } else { + sm_per_multiproc = + _ConvertSMVer2Cores(major, minor); + } + int multiProcessorCount = 0, clockRate = 0; + checkCudaErrors(cudaDeviceGetAttribute(&multiProcessorCount, cudaDevAttrMultiProcessorCount, current_device)); + cudaError_t result = cudaDeviceGetAttribute(&clockRate, cudaDevAttrClockRate, current_device); + if (result != cudaSuccess) { + // If cudaDevAttrClockRate attribute is not supported we + // set clockRate as 1, to consider GPU with most SMs and CUDA Cores. + if(result == cudaErrorInvalidValue) { + clockRate = 1; + } + else { + fprintf(stderr, "CUDA error at %s:%d code=%d(%s) \n", __FILE__, __LINE__, + static_cast(result), _cudaGetErrorEnum(result)); + exit(EXIT_FAILURE); + } + } + uint64_t compute_perf = (uint64_t)multiProcessorCount * sm_per_multiproc * clockRate; + + if (compute_perf > max_compute_perf) { + max_compute_perf = compute_perf; + max_perf_device = current_device; + } + } else { + devices_prohibited++; + } + + ++current_device; + } + + if (devices_prohibited == device_count) { + fprintf(stderr, + "gpuGetMaxGflopsDeviceId() CUDA error:" + " all devices have compute mode prohibited.\n"); + exit(EXIT_FAILURE); + } + + return max_perf_device; +} + +// Initialization code to find the best CUDA Device +inline int findCudaDevice(int argc, const char **argv) { + int devID = 0; + + // If the command-line has a device number specified, use it + if (checkCmdLineFlag(argc, argv, "device")) { + devID = getCmdLineArgumentInt(argc, argv, "device="); + + if (devID < 0) { + printf("Invalid command line parameter\n "); + exit(EXIT_FAILURE); + } else { + devID = gpuDeviceInit(devID); + + if (devID < 0) { + printf("exiting...\n"); + exit(EXIT_FAILURE); + } + } + } else { + // Otherwise pick the device with highest Gflops/s + devID = gpuGetMaxGflopsDeviceId(); + checkCudaErrors(cudaSetDevice(devID)); + int major = 0, minor = 0; + checkCudaErrors(cudaDeviceGetAttribute(&major, cudaDevAttrComputeCapabilityMajor, devID)); + checkCudaErrors(cudaDeviceGetAttribute(&minor, cudaDevAttrComputeCapabilityMinor, devID)); + printf("GPU Device %d: \"%s\" with compute capability %d.%d\n\n", + devID, _ConvertSMVer2ArchName(major, minor), major, minor); + + } + + return devID; +} + +inline int findIntegratedGPU() { + int current_device = 0; + int device_count = 0; + int devices_prohibited = 0; + + checkCudaErrors(cudaGetDeviceCount(&device_count)); + + if (device_count == 0) { + fprintf(stderr, "CUDA error: no devices supporting CUDA.\n"); + exit(EXIT_FAILURE); + } + + // Find the integrated GPU which is compute capable + while (current_device < device_count) { + int computeMode = -1, integrated = -1; + checkCudaErrors(cudaDeviceGetAttribute(&computeMode, cudaDevAttrComputeMode, current_device)); + checkCudaErrors(cudaDeviceGetAttribute(&integrated, cudaDevAttrIntegrated, current_device)); + // If GPU is integrated and is not running on Compute Mode prohibited, + // then cuda can map to GLES resource + if (integrated && (computeMode != cudaComputeModeProhibited)) { + checkCudaErrors(cudaSetDevice(current_device)); + + int major = 0, minor = 0; + checkCudaErrors(cudaDeviceGetAttribute(&major, cudaDevAttrComputeCapabilityMajor, current_device)); + checkCudaErrors(cudaDeviceGetAttribute(&minor, cudaDevAttrComputeCapabilityMinor, current_device)); + printf("GPU Device %d: \"%s\" with compute capability %d.%d\n\n", + current_device, _ConvertSMVer2ArchName(major, minor), major, minor); + + return current_device; + } else { + devices_prohibited++; + } + + current_device++; + } + + if (devices_prohibited == device_count) { + fprintf(stderr, + "CUDA error:" + " No GLES-CUDA Interop capable GPU found.\n"); + exit(EXIT_FAILURE); + } + + return -1; +} + +// General check for CUDA GPU SM Capabilities +inline bool checkCudaCapabilities(int major_version, int minor_version) { + int dev; + int major = 0, minor = 0; + + checkCudaErrors(cudaGetDevice(&dev)); + checkCudaErrors(cudaDeviceGetAttribute(&major, cudaDevAttrComputeCapabilityMajor, dev)); + checkCudaErrors(cudaDeviceGetAttribute(&minor, cudaDevAttrComputeCapabilityMinor, dev)); + + if ((major > major_version) || + (major == major_version && + minor >= minor_version)) { + printf(" Device %d: <%16s >, Compute SM %d.%d detected\n", dev, + _ConvertSMVer2ArchName(major, minor), major, minor); + return true; + } else { + printf( + " No GPU device was found that can support " + "CUDA compute capability %d.%d.\n", + major_version, minor_version); + return false; + } +} +#endif + + // end of CUDA Helper Functions + +#endif // COMMON_HELPER_CUDA_H_ diff --git a/phitest/render/cuda/src/cuda-samples/Common/helper_math.h b/phitest/render/cuda/src/cuda-samples/Common/helper_math.h new file mode 100644 index 0000000..cc1e594 --- /dev/null +++ b/phitest/render/cuda/src/cuda-samples/Common/helper_math.h @@ -0,0 +1,1469 @@ +/* Copyright (c) 2019, NVIDIA CORPORATION. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of NVIDIA CORPORATION nor the names of its + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/* + * This file implements common mathematical operations on vector types + * (float3, float4 etc.) since these are not provided as standard by CUDA. + * + * The syntax is modeled on the Cg standard library. + * + * This is part of the Helper library includes + * + * Thanks to Linh Hah for additions and fixes. + */ + +#ifndef HELPER_MATH_H +#define HELPER_MATH_H + +#include "cuda_runtime.h" + +typedef unsigned int uint; +typedef unsigned short ushort; + +#ifndef EXIT_WAIVED +#define EXIT_WAIVED 2 +#endif + +#ifndef __CUDACC__ +#include + +//////////////////////////////////////////////////////////////////////////////// +// host implementations of CUDA functions +//////////////////////////////////////////////////////////////////////////////// + +inline float fminf(float a, float b) +{ + return a < b ? a : b; +} + +inline float fmaxf(float a, float b) +{ + return a > b ? a : b; +} + +inline int max(int a, int b) +{ + return a > b ? a : b; +} + +inline int min(int a, int b) +{ + return a < b ? a : b; +} + +inline float rsqrtf(float x) +{ + return 1.0f / sqrtf(x); +} +#endif + +//////////////////////////////////////////////////////////////////////////////// +// constructors +//////////////////////////////////////////////////////////////////////////////// + +inline __host__ __device__ float2 make_float2(float s) +{ + return make_float2(s, s); +} +inline __host__ __device__ float2 make_float2(float3 a) +{ + return make_float2(a.x, a.y); +} +inline __host__ __device__ float2 make_float2(int2 a) +{ + return make_float2(float(a.x), float(a.y)); +} +inline __host__ __device__ float2 make_float2(uint2 a) +{ + return make_float2(float(a.x), float(a.y)); +} + +inline __host__ __device__ int2 make_int2(int s) +{ + return make_int2(s, s); +} +inline __host__ __device__ int2 make_int2(int3 a) +{ + return make_int2(a.x, a.y); +} +inline __host__ __device__ int2 make_int2(uint2 a) +{ + return make_int2(int(a.x), int(a.y)); +} +inline __host__ __device__ int2 make_int2(float2 a) +{ + return make_int2(int(a.x), int(a.y)); +} + +inline __host__ __device__ uint2 make_uint2(uint s) +{ + return make_uint2(s, s); +} +inline __host__ __device__ uint2 make_uint2(uint3 a) +{ + return make_uint2(a.x, a.y); +} +inline __host__ __device__ uint2 make_uint2(int2 a) +{ + return make_uint2(uint(a.x), uint(a.y)); +} + +inline __host__ __device__ float3 make_float3(float s) +{ + return make_float3(s, s, s); +} +inline __host__ __device__ float3 make_float3(float2 a) +{ + return make_float3(a.x, a.y, 0.0f); +} +inline __host__ __device__ float3 make_float3(float2 a, float s) +{ + return make_float3(a.x, a.y, s); +} +inline __host__ __device__ float3 make_float3(float4 a) +{ + return make_float3(a.x, a.y, a.z); +} +inline __host__ __device__ float3 make_float3(int3 a) +{ + return make_float3(float(a.x), float(a.y), float(a.z)); +} +inline __host__ __device__ float3 make_float3(uint3 a) +{ + return make_float3(float(a.x), float(a.y), float(a.z)); +} + +inline __host__ __device__ int3 make_int3(int s) +{ + return make_int3(s, s, s); +} +inline __host__ __device__ int3 make_int3(int2 a) +{ + return make_int3(a.x, a.y, 0); +} +inline __host__ __device__ int3 make_int3(int2 a, int s) +{ + return make_int3(a.x, a.y, s); +} +inline __host__ __device__ int3 make_int3(uint3 a) +{ + return make_int3(int(a.x), int(a.y), int(a.z)); +} +inline __host__ __device__ int3 make_int3(float3 a) +{ + return make_int3(int(a.x), int(a.y), int(a.z)); +} + +inline __host__ __device__ uint3 make_uint3(uint s) +{ + return make_uint3(s, s, s); +} +inline __host__ __device__ uint3 make_uint3(uint2 a) +{ + return make_uint3(a.x, a.y, 0); +} +inline __host__ __device__ uint3 make_uint3(uint2 a, uint s) +{ + return make_uint3(a.x, a.y, s); +} +inline __host__ __device__ uint3 make_uint3(uint4 a) +{ + return make_uint3(a.x, a.y, a.z); +} +inline __host__ __device__ uint3 make_uint3(int3 a) +{ + return make_uint3(uint(a.x), uint(a.y), uint(a.z)); +} + +inline __host__ __device__ float4 make_float4(float s) +{ + return make_float4(s, s, s, s); +} +inline __host__ __device__ float4 make_float4(float3 a) +{ + return make_float4(a.x, a.y, a.z, 0.0f); +} +inline __host__ __device__ float4 make_float4(float3 a, float w) +{ + return make_float4(a.x, a.y, a.z, w); +} +inline __host__ __device__ float4 make_float4(int4 a) +{ + return make_float4(float(a.x), float(a.y), float(a.z), float(a.w)); +} +inline __host__ __device__ float4 make_float4(uint4 a) +{ + return make_float4(float(a.x), float(a.y), float(a.z), float(a.w)); +} + +inline __host__ __device__ int4 make_int4(int s) +{ + return make_int4(s, s, s, s); +} +inline __host__ __device__ int4 make_int4(int3 a) +{ + return make_int4(a.x, a.y, a.z, 0); +} +inline __host__ __device__ int4 make_int4(int3 a, int w) +{ + return make_int4(a.x, a.y, a.z, w); +} +inline __host__ __device__ int4 make_int4(uint4 a) +{ + return make_int4(int(a.x), int(a.y), int(a.z), int(a.w)); +} +inline __host__ __device__ int4 make_int4(float4 a) +{ + return make_int4(int(a.x), int(a.y), int(a.z), int(a.w)); +} + + +inline __host__ __device__ uint4 make_uint4(uint s) +{ + return make_uint4(s, s, s, s); +} +inline __host__ __device__ uint4 make_uint4(uint3 a) +{ + return make_uint4(a.x, a.y, a.z, 0); +} +inline __host__ __device__ uint4 make_uint4(uint3 a, uint w) +{ + return make_uint4(a.x, a.y, a.z, w); +} +inline __host__ __device__ uint4 make_uint4(int4 a) +{ + return make_uint4(uint(a.x), uint(a.y), uint(a.z), uint(a.w)); +} + +//////////////////////////////////////////////////////////////////////////////// +// negate +//////////////////////////////////////////////////////////////////////////////// + +inline __host__ __device__ float2 operator-(float2 &a) +{ + return make_float2(-a.x, -a.y); +} +inline __host__ __device__ int2 operator-(int2 &a) +{ + return make_int2(-a.x, -a.y); +} +inline __host__ __device__ float3 operator-(float3 &a) +{ + return make_float3(-a.x, -a.y, -a.z); +} +inline __host__ __device__ int3 operator-(int3 &a) +{ + return make_int3(-a.x, -a.y, -a.z); +} +inline __host__ __device__ float4 operator-(float4 &a) +{ + return make_float4(-a.x, -a.y, -a.z, -a.w); +} +inline __host__ __device__ int4 operator-(int4 &a) +{ + return make_int4(-a.x, -a.y, -a.z, -a.w); +} + +//////////////////////////////////////////////////////////////////////////////// +// addition +//////////////////////////////////////////////////////////////////////////////// + +inline __host__ __device__ float2 operator+(float2 a, float2 b) +{ + return make_float2(a.x + b.x, a.y + b.y); +} +inline __host__ __device__ void operator+=(float2 &a, float2 b) +{ + a.x += b.x; + a.y += b.y; +} +inline __host__ __device__ float2 operator+(float2 a, float b) +{ + return make_float2(a.x + b, a.y + b); +} +inline __host__ __device__ float2 operator+(float b, float2 a) +{ + return make_float2(a.x + b, a.y + b); +} +inline __host__ __device__ void operator+=(float2 &a, float b) +{ + a.x += b; + a.y += b; +} + +inline __host__ __device__ int2 operator+(int2 a, int2 b) +{ + return make_int2(a.x + b.x, a.y + b.y); +} +inline __host__ __device__ void operator+=(int2 &a, int2 b) +{ + a.x += b.x; + a.y += b.y; +} +inline __host__ __device__ int2 operator+(int2 a, int b) +{ + return make_int2(a.x + b, a.y + b); +} +inline __host__ __device__ int2 operator+(int b, int2 a) +{ + return make_int2(a.x + b, a.y + b); +} +inline __host__ __device__ void operator+=(int2 &a, int b) +{ + a.x += b; + a.y += b; +} + +inline __host__ __device__ uint2 operator+(uint2 a, uint2 b) +{ + return make_uint2(a.x + b.x, a.y + b.y); +} +inline __host__ __device__ void operator+=(uint2 &a, uint2 b) +{ + a.x += b.x; + a.y += b.y; +} +inline __host__ __device__ uint2 operator+(uint2 a, uint b) +{ + return make_uint2(a.x + b, a.y + b); +} +inline __host__ __device__ uint2 operator+(uint b, uint2 a) +{ + return make_uint2(a.x + b, a.y + b); +} +inline __host__ __device__ void operator+=(uint2 &a, uint b) +{ + a.x += b; + a.y += b; +} + + +inline __host__ __device__ float3 operator+(float3 a, float3 b) +{ + return make_float3(a.x + b.x, a.y + b.y, a.z + b.z); +} +inline __host__ __device__ void operator+=(float3 &a, float3 b) +{ + a.x += b.x; + a.y += b.y; + a.z += b.z; +} +inline __host__ __device__ float3 operator+(float3 a, float b) +{ + return make_float3(a.x + b, a.y + b, a.z + b); +} +inline __host__ __device__ void operator+=(float3 &a, float b) +{ + a.x += b; + a.y += b; + a.z += b; +} + +inline __host__ __device__ int3 operator+(int3 a, int3 b) +{ + return make_int3(a.x + b.x, a.y + b.y, a.z + b.z); +} +inline __host__ __device__ void operator+=(int3 &a, int3 b) +{ + a.x += b.x; + a.y += b.y; + a.z += b.z; +} +inline __host__ __device__ int3 operator+(int3 a, int b) +{ + return make_int3(a.x + b, a.y + b, a.z + b); +} +inline __host__ __device__ void operator+=(int3 &a, int b) +{ + a.x += b; + a.y += b; + a.z += b; +} + +inline __host__ __device__ uint3 operator+(uint3 a, uint3 b) +{ + return make_uint3(a.x + b.x, a.y + b.y, a.z + b.z); +} +inline __host__ __device__ void operator+=(uint3 &a, uint3 b) +{ + a.x += b.x; + a.y += b.y; + a.z += b.z; +} +inline __host__ __device__ uint3 operator+(uint3 a, uint b) +{ + return make_uint3(a.x + b, a.y + b, a.z + b); +} +inline __host__ __device__ void operator+=(uint3 &a, uint b) +{ + a.x += b; + a.y += b; + a.z += b; +} + +inline __host__ __device__ int3 operator+(int b, int3 a) +{ + return make_int3(a.x + b, a.y + b, a.z + b); +} +inline __host__ __device__ uint3 operator+(uint b, uint3 a) +{ + return make_uint3(a.x + b, a.y + b, a.z + b); +} +inline __host__ __device__ float3 operator+(float b, float3 a) +{ + return make_float3(a.x + b, a.y + b, a.z + b); +} + +inline __host__ __device__ float4 operator+(float4 a, float4 b) +{ + return make_float4(a.x + b.x, a.y + b.y, a.z + b.z, a.w + b.w); +} +inline __host__ __device__ void operator+=(float4 &a, float4 b) +{ + a.x += b.x; + a.y += b.y; + a.z += b.z; + a.w += b.w; +} +inline __host__ __device__ float4 operator+(float4 a, float b) +{ + return make_float4(a.x + b, a.y + b, a.z + b, a.w + b); +} +inline __host__ __device__ float4 operator+(float b, float4 a) +{ + return make_float4(a.x + b, a.y + b, a.z + b, a.w + b); +} +inline __host__ __device__ void operator+=(float4 &a, float b) +{ + a.x += b; + a.y += b; + a.z += b; + a.w += b; +} + +inline __host__ __device__ int4 operator+(int4 a, int4 b) +{ + return make_int4(a.x + b.x, a.y + b.y, a.z + b.z, a.w + b.w); +} +inline __host__ __device__ void operator+=(int4 &a, int4 b) +{ + a.x += b.x; + a.y += b.y; + a.z += b.z; + a.w += b.w; +} +inline __host__ __device__ int4 operator+(int4 a, int b) +{ + return make_int4(a.x + b, a.y + b, a.z + b, a.w + b); +} +inline __host__ __device__ int4 operator+(int b, int4 a) +{ + return make_int4(a.x + b, a.y + b, a.z + b, a.w + b); +} +inline __host__ __device__ void operator+=(int4 &a, int b) +{ + a.x += b; + a.y += b; + a.z += b; + a.w += b; +} + +inline __host__ __device__ uint4 operator+(uint4 a, uint4 b) +{ + return make_uint4(a.x + b.x, a.y + b.y, a.z + b.z, a.w + b.w); +} +inline __host__ __device__ void operator+=(uint4 &a, uint4 b) +{ + a.x += b.x; + a.y += b.y; + a.z += b.z; + a.w += b.w; +} +inline __host__ __device__ uint4 operator+(uint4 a, uint b) +{ + return make_uint4(a.x + b, a.y + b, a.z + b, a.w + b); +} +inline __host__ __device__ uint4 operator+(uint b, uint4 a) +{ + return make_uint4(a.x + b, a.y + b, a.z + b, a.w + b); +} +inline __host__ __device__ void operator+=(uint4 &a, uint b) +{ + a.x += b; + a.y += b; + a.z += b; + a.w += b; +} + +//////////////////////////////////////////////////////////////////////////////// +// subtract +//////////////////////////////////////////////////////////////////////////////// + +inline __host__ __device__ float2 operator-(float2 a, float2 b) +{ + return make_float2(a.x - b.x, a.y - b.y); +} +inline __host__ __device__ void operator-=(float2 &a, float2 b) +{ + a.x -= b.x; + a.y -= b.y; +} +inline __host__ __device__ float2 operator-(float2 a, float b) +{ + return make_float2(a.x - b, a.y - b); +} +inline __host__ __device__ float2 operator-(float b, float2 a) +{ + return make_float2(b - a.x, b - a.y); +} +inline __host__ __device__ void operator-=(float2 &a, float b) +{ + a.x -= b; + a.y -= b; +} + +inline __host__ __device__ int2 operator-(int2 a, int2 b) +{ + return make_int2(a.x - b.x, a.y - b.y); +} +inline __host__ __device__ void operator-=(int2 &a, int2 b) +{ + a.x -= b.x; + a.y -= b.y; +} +inline __host__ __device__ int2 operator-(int2 a, int b) +{ + return make_int2(a.x - b, a.y - b); +} +inline __host__ __device__ int2 operator-(int b, int2 a) +{ + return make_int2(b - a.x, b - a.y); +} +inline __host__ __device__ void operator-=(int2 &a, int b) +{ + a.x -= b; + a.y -= b; +} + +inline __host__ __device__ uint2 operator-(uint2 a, uint2 b) +{ + return make_uint2(a.x - b.x, a.y - b.y); +} +inline __host__ __device__ void operator-=(uint2 &a, uint2 b) +{ + a.x -= b.x; + a.y -= b.y; +} +inline __host__ __device__ uint2 operator-(uint2 a, uint b) +{ + return make_uint2(a.x - b, a.y - b); +} +inline __host__ __device__ uint2 operator-(uint b, uint2 a) +{ + return make_uint2(b - a.x, b - a.y); +} +inline __host__ __device__ void operator-=(uint2 &a, uint b) +{ + a.x -= b; + a.y -= b; +} + +inline __host__ __device__ float3 operator-(float3 a, float3 b) +{ + return make_float3(a.x - b.x, a.y - b.y, a.z - b.z); +} +inline __host__ __device__ void operator-=(float3 &a, float3 b) +{ + a.x -= b.x; + a.y -= b.y; + a.z -= b.z; +} +inline __host__ __device__ float3 operator-(float3 a, float b) +{ + return make_float3(a.x - b, a.y - b, a.z - b); +} +inline __host__ __device__ float3 operator-(float b, float3 a) +{ + return make_float3(b - a.x, b - a.y, b - a.z); +} +inline __host__ __device__ void operator-=(float3 &a, float b) +{ + a.x -= b; + a.y -= b; + a.z -= b; +} + +inline __host__ __device__ int3 operator-(int3 a, int3 b) +{ + return make_int3(a.x - b.x, a.y - b.y, a.z - b.z); +} +inline __host__ __device__ void operator-=(int3 &a, int3 b) +{ + a.x -= b.x; + a.y -= b.y; + a.z -= b.z; +} +inline __host__ __device__ int3 operator-(int3 a, int b) +{ + return make_int3(a.x - b, a.y - b, a.z - b); +} +inline __host__ __device__ int3 operator-(int b, int3 a) +{ + return make_int3(b - a.x, b - a.y, b - a.z); +} +inline __host__ __device__ void operator-=(int3 &a, int b) +{ + a.x -= b; + a.y -= b; + a.z -= b; +} + +inline __host__ __device__ uint3 operator-(uint3 a, uint3 b) +{ + return make_uint3(a.x - b.x, a.y - b.y, a.z - b.z); +} +inline __host__ __device__ void operator-=(uint3 &a, uint3 b) +{ + a.x -= b.x; + a.y -= b.y; + a.z -= b.z; +} +inline __host__ __device__ uint3 operator-(uint3 a, uint b) +{ + return make_uint3(a.x - b, a.y - b, a.z - b); +} +inline __host__ __device__ uint3 operator-(uint b, uint3 a) +{ + return make_uint3(b - a.x, b - a.y, b - a.z); +} +inline __host__ __device__ void operator-=(uint3 &a, uint b) +{ + a.x -= b; + a.y -= b; + a.z -= b; +} + +inline __host__ __device__ float4 operator-(float4 a, float4 b) +{ + return make_float4(a.x - b.x, a.y - b.y, a.z - b.z, a.w - b.w); +} +inline __host__ __device__ void operator-=(float4 &a, float4 b) +{ + a.x -= b.x; + a.y -= b.y; + a.z -= b.z; + a.w -= b.w; +} +inline __host__ __device__ float4 operator-(float4 a, float b) +{ + return make_float4(a.x - b, a.y - b, a.z - b, a.w - b); +} +inline __host__ __device__ void operator-=(float4 &a, float b) +{ + a.x -= b; + a.y -= b; + a.z -= b; + a.w -= b; +} + +inline __host__ __device__ int4 operator-(int4 a, int4 b) +{ + return make_int4(a.x - b.x, a.y - b.y, a.z - b.z, a.w - b.w); +} +inline __host__ __device__ void operator-=(int4 &a, int4 b) +{ + a.x -= b.x; + a.y -= b.y; + a.z -= b.z; + a.w -= b.w; +} +inline __host__ __device__ int4 operator-(int4 a, int b) +{ + return make_int4(a.x - b, a.y - b, a.z - b, a.w - b); +} +inline __host__ __device__ int4 operator-(int b, int4 a) +{ + return make_int4(b - a.x, b - a.y, b - a.z, b - a.w); +} +inline __host__ __device__ void operator-=(int4 &a, int b) +{ + a.x -= b; + a.y -= b; + a.z -= b; + a.w -= b; +} + +inline __host__ __device__ uint4 operator-(uint4 a, uint4 b) +{ + return make_uint4(a.x - b.x, a.y - b.y, a.z - b.z, a.w - b.w); +} +inline __host__ __device__ void operator-=(uint4 &a, uint4 b) +{ + a.x -= b.x; + a.y -= b.y; + a.z -= b.z; + a.w -= b.w; +} +inline __host__ __device__ uint4 operator-(uint4 a, uint b) +{ + return make_uint4(a.x - b, a.y - b, a.z - b, a.w - b); +} +inline __host__ __device__ uint4 operator-(uint b, uint4 a) +{ + return make_uint4(b - a.x, b - a.y, b - a.z, b - a.w); +} +inline __host__ __device__ void operator-=(uint4 &a, uint b) +{ + a.x -= b; + a.y -= b; + a.z -= b; + a.w -= b; +} + +//////////////////////////////////////////////////////////////////////////////// +// multiply +//////////////////////////////////////////////////////////////////////////////// + +inline __host__ __device__ float2 operator*(float2 a, float2 b) +{ + return make_float2(a.x * b.x, a.y * b.y); +} +inline __host__ __device__ void operator*=(float2 &a, float2 b) +{ + a.x *= b.x; + a.y *= b.y; +} +inline __host__ __device__ float2 operator*(float2 a, float b) +{ + return make_float2(a.x * b, a.y * b); +} +inline __host__ __device__ float2 operator*(float b, float2 a) +{ + return make_float2(b * a.x, b * a.y); +} +inline __host__ __device__ void operator*=(float2 &a, float b) +{ + a.x *= b; + a.y *= b; +} + +inline __host__ __device__ int2 operator*(int2 a, int2 b) +{ + return make_int2(a.x * b.x, a.y * b.y); +} +inline __host__ __device__ void operator*=(int2 &a, int2 b) +{ + a.x *= b.x; + a.y *= b.y; +} +inline __host__ __device__ int2 operator*(int2 a, int b) +{ + return make_int2(a.x * b, a.y * b); +} +inline __host__ __device__ int2 operator*(int b, int2 a) +{ + return make_int2(b * a.x, b * a.y); +} +inline __host__ __device__ void operator*=(int2 &a, int b) +{ + a.x *= b; + a.y *= b; +} + +inline __host__ __device__ uint2 operator*(uint2 a, uint2 b) +{ + return make_uint2(a.x * b.x, a.y * b.y); +} +inline __host__ __device__ void operator*=(uint2 &a, uint2 b) +{ + a.x *= b.x; + a.y *= b.y; +} +inline __host__ __device__ uint2 operator*(uint2 a, uint b) +{ + return make_uint2(a.x * b, a.y * b); +} +inline __host__ __device__ uint2 operator*(uint b, uint2 a) +{ + return make_uint2(b * a.x, b * a.y); +} +inline __host__ __device__ void operator*=(uint2 &a, uint b) +{ + a.x *= b; + a.y *= b; +} + +inline __host__ __device__ float3 operator*(float3 a, float3 b) +{ + return make_float3(a.x * b.x, a.y * b.y, a.z * b.z); +} +inline __host__ __device__ void operator*=(float3 &a, float3 b) +{ + a.x *= b.x; + a.y *= b.y; + a.z *= b.z; +} +inline __host__ __device__ float3 operator*(float3 a, float b) +{ + return make_float3(a.x * b, a.y * b, a.z * b); +} +inline __host__ __device__ float3 operator*(float b, float3 a) +{ + return make_float3(b * a.x, b * a.y, b * a.z); +} +inline __host__ __device__ void operator*=(float3 &a, float b) +{ + a.x *= b; + a.y *= b; + a.z *= b; +} + +inline __host__ __device__ int3 operator*(int3 a, int3 b) +{ + return make_int3(a.x * b.x, a.y * b.y, a.z * b.z); +} +inline __host__ __device__ void operator*=(int3 &a, int3 b) +{ + a.x *= b.x; + a.y *= b.y; + a.z *= b.z; +} +inline __host__ __device__ int3 operator*(int3 a, int b) +{ + return make_int3(a.x * b, a.y * b, a.z * b); +} +inline __host__ __device__ int3 operator*(int b, int3 a) +{ + return make_int3(b * a.x, b * a.y, b * a.z); +} +inline __host__ __device__ void operator*=(int3 &a, int b) +{ + a.x *= b; + a.y *= b; + a.z *= b; +} + +inline __host__ __device__ uint3 operator*(uint3 a, uint3 b) +{ + return make_uint3(a.x * b.x, a.y * b.y, a.z * b.z); +} +inline __host__ __device__ void operator*=(uint3 &a, uint3 b) +{ + a.x *= b.x; + a.y *= b.y; + a.z *= b.z; +} +inline __host__ __device__ uint3 operator*(uint3 a, uint b) +{ + return make_uint3(a.x * b, a.y * b, a.z * b); +} +inline __host__ __device__ uint3 operator*(uint b, uint3 a) +{ + return make_uint3(b * a.x, b * a.y, b * a.z); +} +inline __host__ __device__ void operator*=(uint3 &a, uint b) +{ + a.x *= b; + a.y *= b; + a.z *= b; +} + +inline __host__ __device__ float4 operator*(float4 a, float4 b) +{ + return make_float4(a.x * b.x, a.y * b.y, a.z * b.z, a.w * b.w); +} +inline __host__ __device__ void operator*=(float4 &a, float4 b) +{ + a.x *= b.x; + a.y *= b.y; + a.z *= b.z; + a.w *= b.w; +} +inline __host__ __device__ float4 operator*(float4 a, float b) +{ + return make_float4(a.x * b, a.y * b, a.z * b, a.w * b); +} +inline __host__ __device__ float4 operator*(float b, float4 a) +{ + return make_float4(b * a.x, b * a.y, b * a.z, b * a.w); +} +inline __host__ __device__ void operator*=(float4 &a, float b) +{ + a.x *= b; + a.y *= b; + a.z *= b; + a.w *= b; +} + +inline __host__ __device__ int4 operator*(int4 a, int4 b) +{ + return make_int4(a.x * b.x, a.y * b.y, a.z * b.z, a.w * b.w); +} +inline __host__ __device__ void operator*=(int4 &a, int4 b) +{ + a.x *= b.x; + a.y *= b.y; + a.z *= b.z; + a.w *= b.w; +} +inline __host__ __device__ int4 operator*(int4 a, int b) +{ + return make_int4(a.x * b, a.y * b, a.z * b, a.w * b); +} +inline __host__ __device__ int4 operator*(int b, int4 a) +{ + return make_int4(b * a.x, b * a.y, b * a.z, b * a.w); +} +inline __host__ __device__ void operator*=(int4 &a, int b) +{ + a.x *= b; + a.y *= b; + a.z *= b; + a.w *= b; +} + +inline __host__ __device__ uint4 operator*(uint4 a, uint4 b) +{ + return make_uint4(a.x * b.x, a.y * b.y, a.z * b.z, a.w * b.w); +} +inline __host__ __device__ void operator*=(uint4 &a, uint4 b) +{ + a.x *= b.x; + a.y *= b.y; + a.z *= b.z; + a.w *= b.w; +} +inline __host__ __device__ uint4 operator*(uint4 a, uint b) +{ + return make_uint4(a.x * b, a.y * b, a.z * b, a.w * b); +} +inline __host__ __device__ uint4 operator*(uint b, uint4 a) +{ + return make_uint4(b * a.x, b * a.y, b * a.z, b * a.w); +} +inline __host__ __device__ void operator*=(uint4 &a, uint b) +{ + a.x *= b; + a.y *= b; + a.z *= b; + a.w *= b; +} + +//////////////////////////////////////////////////////////////////////////////// +// divide +//////////////////////////////////////////////////////////////////////////////// + +inline __host__ __device__ float2 operator/(float2 a, float2 b) +{ + return make_float2(a.x / b.x, a.y / b.y); +} +inline __host__ __device__ void operator/=(float2 &a, float2 b) +{ + a.x /= b.x; + a.y /= b.y; +} +inline __host__ __device__ float2 operator/(float2 a, float b) +{ + return make_float2(a.x / b, a.y / b); +} +inline __host__ __device__ void operator/=(float2 &a, float b) +{ + a.x /= b; + a.y /= b; +} +inline __host__ __device__ float2 operator/(float b, float2 a) +{ + return make_float2(b / a.x, b / a.y); +} + +inline __host__ __device__ float3 operator/(float3 a, float3 b) +{ + return make_float3(a.x / b.x, a.y / b.y, a.z / b.z); +} +inline __host__ __device__ void operator/=(float3 &a, float3 b) +{ + a.x /= b.x; + a.y /= b.y; + a.z /= b.z; +} +inline __host__ __device__ float3 operator/(float3 a, float b) +{ + return make_float3(a.x / b, a.y / b, a.z / b); +} +inline __host__ __device__ void operator/=(float3 &a, float b) +{ + a.x /= b; + a.y /= b; + a.z /= b; +} +inline __host__ __device__ float3 operator/(float b, float3 a) +{ + return make_float3(b / a.x, b / a.y, b / a.z); +} + +inline __host__ __device__ float4 operator/(float4 a, float4 b) +{ + return make_float4(a.x / b.x, a.y / b.y, a.z / b.z, a.w / b.w); +} +inline __host__ __device__ void operator/=(float4 &a, float4 b) +{ + a.x /= b.x; + a.y /= b.y; + a.z /= b.z; + a.w /= b.w; +} +inline __host__ __device__ float4 operator/(float4 a, float b) +{ + return make_float4(a.x / b, a.y / b, a.z / b, a.w / b); +} +inline __host__ __device__ void operator/=(float4 &a, float b) +{ + a.x /= b; + a.y /= b; + a.z /= b; + a.w /= b; +} +inline __host__ __device__ float4 operator/(float b, float4 a) +{ + return make_float4(b / a.x, b / a.y, b / a.z, b / a.w); +} + +//////////////////////////////////////////////////////////////////////////////// +// min +//////////////////////////////////////////////////////////////////////////////// + +inline __host__ __device__ float2 fminf(float2 a, float2 b) +{ + return make_float2(fminf(a.x,b.x), fminf(a.y,b.y)); +} +inline __host__ __device__ float3 fminf(float3 a, float3 b) +{ + return make_float3(fminf(a.x,b.x), fminf(a.y,b.y), fminf(a.z,b.z)); +} +inline __host__ __device__ float4 fminf(float4 a, float4 b) +{ + return make_float4(fminf(a.x,b.x), fminf(a.y,b.y), fminf(a.z,b.z), fminf(a.w,b.w)); +} + +inline __host__ __device__ int2 min(int2 a, int2 b) +{ + return make_int2(min(a.x,b.x), min(a.y,b.y)); +} +inline __host__ __device__ int3 min(int3 a, int3 b) +{ + return make_int3(min(a.x,b.x), min(a.y,b.y), min(a.z,b.z)); +} +inline __host__ __device__ int4 min(int4 a, int4 b) +{ + return make_int4(min(a.x,b.x), min(a.y,b.y), min(a.z,b.z), min(a.w,b.w)); +} + +inline __host__ __device__ uint2 min(uint2 a, uint2 b) +{ + return make_uint2(min(a.x,b.x), min(a.y,b.y)); +} +inline __host__ __device__ uint3 min(uint3 a, uint3 b) +{ + return make_uint3(min(a.x,b.x), min(a.y,b.y), min(a.z,b.z)); +} +inline __host__ __device__ uint4 min(uint4 a, uint4 b) +{ + return make_uint4(min(a.x,b.x), min(a.y,b.y), min(a.z,b.z), min(a.w,b.w)); +} + +//////////////////////////////////////////////////////////////////////////////// +// max +//////////////////////////////////////////////////////////////////////////////// + +inline __host__ __device__ float2 fmaxf(float2 a, float2 b) +{ + return make_float2(fmaxf(a.x,b.x), fmaxf(a.y,b.y)); +} +inline __host__ __device__ float3 fmaxf(float3 a, float3 b) +{ + return make_float3(fmaxf(a.x,b.x), fmaxf(a.y,b.y), fmaxf(a.z,b.z)); +} +inline __host__ __device__ float4 fmaxf(float4 a, float4 b) +{ + return make_float4(fmaxf(a.x,b.x), fmaxf(a.y,b.y), fmaxf(a.z,b.z), fmaxf(a.w,b.w)); +} + +inline __host__ __device__ int2 max(int2 a, int2 b) +{ + return make_int2(max(a.x,b.x), max(a.y,b.y)); +} +inline __host__ __device__ int3 max(int3 a, int3 b) +{ + return make_int3(max(a.x,b.x), max(a.y,b.y), max(a.z,b.z)); +} +inline __host__ __device__ int4 max(int4 a, int4 b) +{ + return make_int4(max(a.x,b.x), max(a.y,b.y), max(a.z,b.z), max(a.w,b.w)); +} + +inline __host__ __device__ uint2 max(uint2 a, uint2 b) +{ + return make_uint2(max(a.x,b.x), max(a.y,b.y)); +} +inline __host__ __device__ uint3 max(uint3 a, uint3 b) +{ + return make_uint3(max(a.x,b.x), max(a.y,b.y), max(a.z,b.z)); +} +inline __host__ __device__ uint4 max(uint4 a, uint4 b) +{ + return make_uint4(max(a.x,b.x), max(a.y,b.y), max(a.z,b.z), max(a.w,b.w)); +} + +//////////////////////////////////////////////////////////////////////////////// +// lerp +// - linear interpolation between a and b, based on value t in [0, 1] range +//////////////////////////////////////////////////////////////////////////////// + +inline __device__ __host__ float lerp(float a, float b, float t) +{ + return a + t*(b-a); +} +inline __device__ __host__ float2 lerp(float2 a, float2 b, float t) +{ + return a + t*(b-a); +} +inline __device__ __host__ float3 lerp(float3 a, float3 b, float t) +{ + return a + t*(b-a); +} +inline __device__ __host__ float4 lerp(float4 a, float4 b, float t) +{ + return a + t*(b-a); +} + +//////////////////////////////////////////////////////////////////////////////// +// clamp +// - clamp the value v to be in the range [a, b] +//////////////////////////////////////////////////////////////////////////////// + +inline __device__ __host__ float clamp(float f, float a, float b) +{ + return fmaxf(a, fminf(f, b)); +} +inline __device__ __host__ int clamp(int f, int a, int b) +{ + return max(a, min(f, b)); +} +inline __device__ __host__ uint clamp(uint f, uint a, uint b) +{ + return max(a, min(f, b)); +} + +inline __device__ __host__ float2 clamp(float2 v, float a, float b) +{ + return make_float2(clamp(v.x, a, b), clamp(v.y, a, b)); +} +inline __device__ __host__ float2 clamp(float2 v, float2 a, float2 b) +{ + return make_float2(clamp(v.x, a.x, b.x), clamp(v.y, a.y, b.y)); +} +inline __device__ __host__ float3 clamp(float3 v, float a, float b) +{ + return make_float3(clamp(v.x, a, b), clamp(v.y, a, b), clamp(v.z, a, b)); +} +inline __device__ __host__ float3 clamp(float3 v, float3 a, float3 b) +{ + return make_float3(clamp(v.x, a.x, b.x), clamp(v.y, a.y, b.y), clamp(v.z, a.z, b.z)); +} +inline __device__ __host__ float4 clamp(float4 v, float a, float b) +{ + return make_float4(clamp(v.x, a, b), clamp(v.y, a, b), clamp(v.z, a, b), clamp(v.w, a, b)); +} +inline __device__ __host__ float4 clamp(float4 v, float4 a, float4 b) +{ + return make_float4(clamp(v.x, a.x, b.x), clamp(v.y, a.y, b.y), clamp(v.z, a.z, b.z), clamp(v.w, a.w, b.w)); +} + +inline __device__ __host__ int2 clamp(int2 v, int a, int b) +{ + return make_int2(clamp(v.x, a, b), clamp(v.y, a, b)); +} +inline __device__ __host__ int2 clamp(int2 v, int2 a, int2 b) +{ + return make_int2(clamp(v.x, a.x, b.x), clamp(v.y, a.y, b.y)); +} +inline __device__ __host__ int3 clamp(int3 v, int a, int b) +{ + return make_int3(clamp(v.x, a, b), clamp(v.y, a, b), clamp(v.z, a, b)); +} +inline __device__ __host__ int3 clamp(int3 v, int3 a, int3 b) +{ + return make_int3(clamp(v.x, a.x, b.x), clamp(v.y, a.y, b.y), clamp(v.z, a.z, b.z)); +} +inline __device__ __host__ int4 clamp(int4 v, int a, int b) +{ + return make_int4(clamp(v.x, a, b), clamp(v.y, a, b), clamp(v.z, a, b), clamp(v.w, a, b)); +} +inline __device__ __host__ int4 clamp(int4 v, int4 a, int4 b) +{ + return make_int4(clamp(v.x, a.x, b.x), clamp(v.y, a.y, b.y), clamp(v.z, a.z, b.z), clamp(v.w, a.w, b.w)); +} + +inline __device__ __host__ uint2 clamp(uint2 v, uint a, uint b) +{ + return make_uint2(clamp(v.x, a, b), clamp(v.y, a, b)); +} +inline __device__ __host__ uint2 clamp(uint2 v, uint2 a, uint2 b) +{ + return make_uint2(clamp(v.x, a.x, b.x), clamp(v.y, a.y, b.y)); +} +inline __device__ __host__ uint3 clamp(uint3 v, uint a, uint b) +{ + return make_uint3(clamp(v.x, a, b), clamp(v.y, a, b), clamp(v.z, a, b)); +} +inline __device__ __host__ uint3 clamp(uint3 v, uint3 a, uint3 b) +{ + return make_uint3(clamp(v.x, a.x, b.x), clamp(v.y, a.y, b.y), clamp(v.z, a.z, b.z)); +} +inline __device__ __host__ uint4 clamp(uint4 v, uint a, uint b) +{ + return make_uint4(clamp(v.x, a, b), clamp(v.y, a, b), clamp(v.z, a, b), clamp(v.w, a, b)); +} +inline __device__ __host__ uint4 clamp(uint4 v, uint4 a, uint4 b) +{ + return make_uint4(clamp(v.x, a.x, b.x), clamp(v.y, a.y, b.y), clamp(v.z, a.z, b.z), clamp(v.w, a.w, b.w)); +} + +//////////////////////////////////////////////////////////////////////////////// +// dot product +//////////////////////////////////////////////////////////////////////////////// + +inline __host__ __device__ float dot(float2 a, float2 b) +{ + return a.x * b.x + a.y * b.y; +} +inline __host__ __device__ float dot(float3 a, float3 b) +{ + return a.x * b.x + a.y * b.y + a.z * b.z; +} +inline __host__ __device__ float dot(float4 a, float4 b) +{ + return a.x * b.x + a.y * b.y + a.z * b.z + a.w * b.w; +} + +inline __host__ __device__ int dot(int2 a, int2 b) +{ + return a.x * b.x + a.y * b.y; +} +inline __host__ __device__ int dot(int3 a, int3 b) +{ + return a.x * b.x + a.y * b.y + a.z * b.z; +} +inline __host__ __device__ int dot(int4 a, int4 b) +{ + return a.x * b.x + a.y * b.y + a.z * b.z + a.w * b.w; +} + +inline __host__ __device__ uint dot(uint2 a, uint2 b) +{ + return a.x * b.x + a.y * b.y; +} +inline __host__ __device__ uint dot(uint3 a, uint3 b) +{ + return a.x * b.x + a.y * b.y + a.z * b.z; +} +inline __host__ __device__ uint dot(uint4 a, uint4 b) +{ + return a.x * b.x + a.y * b.y + a.z * b.z + a.w * b.w; +} + +//////////////////////////////////////////////////////////////////////////////// +// length +//////////////////////////////////////////////////////////////////////////////// + +inline __host__ __device__ float length(float2 v) +{ + return sqrtf(dot(v, v)); +} +inline __host__ __device__ float length(float3 v) +{ + return sqrtf(dot(v, v)); +} +inline __host__ __device__ float length(float4 v) +{ + return sqrtf(dot(v, v)); +} + +//////////////////////////////////////////////////////////////////////////////// +// normalize +//////////////////////////////////////////////////////////////////////////////// + +inline __host__ __device__ float2 normalize(float2 v) +{ + float invLen = rsqrtf(dot(v, v)); + return v * invLen; +} +inline __host__ __device__ float3 normalize(float3 v) +{ + float invLen = rsqrtf(dot(v, v)); + return v * invLen; +} +inline __host__ __device__ float4 normalize(float4 v) +{ + float invLen = rsqrtf(dot(v, v)); + return v * invLen; +} + +//////////////////////////////////////////////////////////////////////////////// +// floor +//////////////////////////////////////////////////////////////////////////////// + +inline __host__ __device__ float2 floorf(float2 v) +{ + return make_float2(floorf(v.x), floorf(v.y)); +} +inline __host__ __device__ float3 floorf(float3 v) +{ + return make_float3(floorf(v.x), floorf(v.y), floorf(v.z)); +} +inline __host__ __device__ float4 floorf(float4 v) +{ + return make_float4(floorf(v.x), floorf(v.y), floorf(v.z), floorf(v.w)); +} + +//////////////////////////////////////////////////////////////////////////////// +// frac - returns the fractional portion of a scalar or each vector component +//////////////////////////////////////////////////////////////////////////////// + +inline __host__ __device__ float fracf(float v) +{ + return v - floorf(v); +} +inline __host__ __device__ float2 fracf(float2 v) +{ + return make_float2(fracf(v.x), fracf(v.y)); +} +inline __host__ __device__ float3 fracf(float3 v) +{ + return make_float3(fracf(v.x), fracf(v.y), fracf(v.z)); +} +inline __host__ __device__ float4 fracf(float4 v) +{ + return make_float4(fracf(v.x), fracf(v.y), fracf(v.z), fracf(v.w)); +} + +//////////////////////////////////////////////////////////////////////////////// +// fmod +//////////////////////////////////////////////////////////////////////////////// + +inline __host__ __device__ float2 fmodf(float2 a, float2 b) +{ + return make_float2(fmodf(a.x, b.x), fmodf(a.y, b.y)); +} +inline __host__ __device__ float3 fmodf(float3 a, float3 b) +{ + return make_float3(fmodf(a.x, b.x), fmodf(a.y, b.y), fmodf(a.z, b.z)); +} +inline __host__ __device__ float4 fmodf(float4 a, float4 b) +{ + return make_float4(fmodf(a.x, b.x), fmodf(a.y, b.y), fmodf(a.z, b.z), fmodf(a.w, b.w)); +} + +//////////////////////////////////////////////////////////////////////////////// +// absolute value +//////////////////////////////////////////////////////////////////////////////// + +inline __host__ __device__ float2 fabs(float2 v) +{ + return make_float2(fabs(v.x), fabs(v.y)); +} +inline __host__ __device__ float3 fabs(float3 v) +{ + return make_float3(fabs(v.x), fabs(v.y), fabs(v.z)); +} +inline __host__ __device__ float4 fabs(float4 v) +{ + return make_float4(fabs(v.x), fabs(v.y), fabs(v.z), fabs(v.w)); +} + +inline __host__ __device__ int2 abs(int2 v) +{ + return make_int2(abs(v.x), abs(v.y)); +} +inline __host__ __device__ int3 abs(int3 v) +{ + return make_int3(abs(v.x), abs(v.y), abs(v.z)); +} +inline __host__ __device__ int4 abs(int4 v) +{ + return make_int4(abs(v.x), abs(v.y), abs(v.z), abs(v.w)); +} + +//////////////////////////////////////////////////////////////////////////////// +// reflect +// - returns reflection of incident ray I around surface normal N +// - N should be normalized, reflected vector's length is equal to length of I +//////////////////////////////////////////////////////////////////////////////// + +inline __host__ __device__ float3 reflect(float3 i, float3 n) +{ + return i - 2.0f * n * dot(n,i); +} + +//////////////////////////////////////////////////////////////////////////////// +// cross product +//////////////////////////////////////////////////////////////////////////////// + +inline __host__ __device__ float3 cross(float3 a, float3 b) +{ + return make_float3(a.y*b.z - a.z*b.y, a.z*b.x - a.x*b.z, a.x*b.y - a.y*b.x); +} + +//////////////////////////////////////////////////////////////////////////////// +// smoothstep +// - returns 0 if x < a +// - returns 1 if x > b +// - otherwise returns smooth interpolation between 0 and 1 based on x +//////////////////////////////////////////////////////////////////////////////// + +inline __device__ __host__ float smoothstep(float a, float b, float x) +{ + float y = clamp((x - a) / (b - a), 0.0f, 1.0f); + return (y*y*(3.0f - (2.0f*y))); +} +inline __device__ __host__ float2 smoothstep(float2 a, float2 b, float2 x) +{ + float2 y = clamp((x - a) / (b - a), 0.0f, 1.0f); + return (y*y*(make_float2(3.0f) - (make_float2(2.0f)*y))); +} +inline __device__ __host__ float3 smoothstep(float3 a, float3 b, float3 x) +{ + float3 y = clamp((x - a) / (b - a), 0.0f, 1.0f); + return (y*y*(make_float3(3.0f) - (make_float3(2.0f)*y))); +} +inline __device__ __host__ float4 smoothstep(float4 a, float4 b, float4 x) +{ + float4 y = clamp((x - a) / (b - a), 0.0f, 1.0f); + return (y*y*(make_float4(3.0f) - (make_float4(2.0f)*y))); +} + +#endif diff --git a/phitest/render/cuda/src/cuda-samples/Common/helper_string.h b/phitest/render/cuda/src/cuda-samples/Common/helper_string.h new file mode 100644 index 0000000..93be081 --- /dev/null +++ b/phitest/render/cuda/src/cuda-samples/Common/helper_string.h @@ -0,0 +1,368 @@ +/* Copyright (c) 2019, NVIDIA CORPORATION. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of NVIDIA CORPORATION nor the names of its + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +// These are helper functions for the SDK samples (string parsing, timers, etc) +#ifndef COMMON_HELPER_STRING_H_ +#define COMMON_HELPER_STRING_H_ + +#include +#include +#include +#include + +#if defined(WIN32) || defined(_WIN32) || defined(WIN64) || defined(_WIN64) +#ifndef _CRT_SECURE_NO_DEPRECATE +#define _CRT_SECURE_NO_DEPRECATE +#endif +#ifndef STRCASECMP +#define STRCASECMP _stricmp +#endif +#ifndef STRNCASECMP +#define STRNCASECMP _strnicmp +#endif +#ifndef STRCPY +#define STRCPY(sFilePath, nLength, sPath) strcpy_s(sFilePath, nLength, sPath) +#endif + +#ifndef FOPEN +#define FOPEN(fHandle, filename, mode) fopen_s(&fHandle, filename, mode) +#endif +#ifndef FOPEN_FAIL +#define FOPEN_FAIL(result) (result != 0) +#endif +#ifndef SSCANF +#define SSCANF sscanf_s +#endif +#ifndef SPRINTF +#define SPRINTF sprintf_s +#endif +#else // Linux Includes +#include +#include + +#ifndef STRCASECMP +#define STRCASECMP strcasecmp +#endif +#ifndef STRNCASECMP +#define STRNCASECMP strncasecmp +#endif +#ifndef STRCPY +#define STRCPY(sFilePath, nLength, sPath) strcpy(sFilePath, sPath) +#endif + +#ifndef FOPEN +#define FOPEN(fHandle, filename, mode) (fHandle = fopen(filename, mode)) +#endif +#ifndef FOPEN_FAIL +#define FOPEN_FAIL(result) (result == NULL) +#endif +#ifndef SSCANF +#define SSCANF sscanf +#endif +#ifndef SPRINTF +#define SPRINTF sprintf +#endif +#endif + +#ifndef EXIT_WAIVED +#define EXIT_WAIVED 2 +#endif + +// CUDA Utility Helper Functions +inline int stringRemoveDelimiter(char delimiter, const char *string) { + int string_start = 0; + + while (string[string_start] == delimiter) { + string_start++; + } + + if (string_start >= static_cast(strlen(string) - 1)) { + return 0; + } + + return string_start; +} + +inline int getFileExtension(char *filename, char **extension) { + int string_length = static_cast(strlen(filename)); + + while (filename[string_length--] != '.') { + if (string_length == 0) break; + } + + if (string_length > 0) string_length += 2; + + if (string_length == 0) + *extension = NULL; + else + *extension = &filename[string_length]; + + return string_length; +} + +inline bool checkCmdLineFlag(const int argc, const char **argv, + const char *string_ref) { + bool bFound = false; + + if (argc >= 1) { + for (int i = 1; i < argc; i++) { + int string_start = stringRemoveDelimiter('-', argv[i]); + const char *string_argv = &argv[i][string_start]; + + const char *equal_pos = strchr(string_argv, '='); + int argv_length = static_cast( + equal_pos == 0 ? strlen(string_argv) : equal_pos - string_argv); + + int length = static_cast(strlen(string_ref)); + + if (length == argv_length && + !STRNCASECMP(string_argv, string_ref, length)) { + bFound = true; + continue; + } + } + } + + return bFound; +} + +// This function wraps the CUDA Driver API into a template function +template +inline bool getCmdLineArgumentValue(const int argc, const char **argv, + const char *string_ref, T *value) { + bool bFound = false; + + if (argc >= 1) { + for (int i = 1; i < argc; i++) { + int string_start = stringRemoveDelimiter('-', argv[i]); + const char *string_argv = &argv[i][string_start]; + int length = static_cast(strlen(string_ref)); + + if (!STRNCASECMP(string_argv, string_ref, length)) { + if (length + 1 <= static_cast(strlen(string_argv))) { + int auto_inc = (string_argv[length] == '=') ? 1 : 0; + *value = (T)atoi(&string_argv[length + auto_inc]); + } + + bFound = true; + i = argc; + } + } + } + + return bFound; +} + +inline int getCmdLineArgumentInt(const int argc, const char **argv, + const char *string_ref) { + bool bFound = false; + int value = -1; + + if (argc >= 1) { + for (int i = 1; i < argc; i++) { + int string_start = stringRemoveDelimiter('-', argv[i]); + const char *string_argv = &argv[i][string_start]; + int length = static_cast(strlen(string_ref)); + + if (!STRNCASECMP(string_argv, string_ref, length)) { + if (length + 1 <= static_cast(strlen(string_argv))) { + int auto_inc = (string_argv[length] == '=') ? 1 : 0; + value = atoi(&string_argv[length + auto_inc]); + } else { + value = 0; + } + + bFound = true; + continue; + } + } + } + + if (bFound) { + return value; + } else { + return 0; + } +} + +inline float getCmdLineArgumentFloat(const int argc, const char **argv, + const char *string_ref) { + bool bFound = false; + float value = -1; + + if (argc >= 1) { + for (int i = 1; i < argc; i++) { + int string_start = stringRemoveDelimiter('-', argv[i]); + const char *string_argv = &argv[i][string_start]; + int length = static_cast(strlen(string_ref)); + + if (!STRNCASECMP(string_argv, string_ref, length)) { + if (length + 1 <= static_cast(strlen(string_argv))) { + int auto_inc = (string_argv[length] == '=') ? 1 : 0; + value = static_cast(atof(&string_argv[length + auto_inc])); + } else { + value = 0.f; + } + + bFound = true; + continue; + } + } + } + + if (bFound) { + return value; + } else { + return 0; + } +} + +inline bool getCmdLineArgumentString(const int argc, const char **argv, + const char *string_ref, + char **string_retval) { + bool bFound = false; + + if (argc >= 1) { + for (int i = 1; i < argc; i++) { + int string_start = stringRemoveDelimiter('-', argv[i]); + char *string_argv = const_cast(&argv[i][string_start]); + int length = static_cast(strlen(string_ref)); + + if (!STRNCASECMP(string_argv, string_ref, length)) { + *string_retval = &string_argv[length + 1]; + bFound = true; + continue; + } + } + } + + if (!bFound) { + *string_retval = NULL; + } + + return bFound; +} + +////////////////////////////////////////////////////////////////////////////// +//! Find the path for a file assuming that +//! files are found in the searchPath. +//! +//! @return the path if succeeded, otherwise 0 +//! @param filename name of the file +//! @param executable_path optional absolute path of the executable +////////////////////////////////////////////////////////////////////////////// +inline char *sdkFindFilePath(const char *filename, + const char *executable_path) { + // defines a variable that is replaced with the name of the + // executable + + // Typical relative search paths to locate needed companion files (e.g. sample + // input data, or JIT source files) The origin for the relative search may be + // the .exe file, a .bat file launching an .exe, a browser .exe launching the + // .exe or .bat, etc + const char *searchPath[] = { + "./", // same dir + "./data/", // same dir + "../../../../Samples//", // up 4 in tree + "../../../Samples//", // up 3 in tree + "../../Samples//", // up 2 in tree + "../../../../Samples//data/", // up 4 in tree + "../../../Samples//data/", // up 3 in tree + "../../Samples//data/", // up 2 in tree + "../../../../Common/data/", // up 4 in tree + "../../../Common/data/", // up 3 in tree + "../../Common/data/" // up 2 in tree + }; + + // Extract the executable name + std::string executable_name; + + if (executable_path != 0) { + executable_name = std::string(executable_path); + +#if defined(WIN32) || defined(_WIN32) || defined(WIN64) || defined(_WIN64) + // Windows path delimiter + size_t delimiter_pos = executable_name.find_last_of('\\'); + executable_name.erase(0, delimiter_pos + 1); + + if (executable_name.rfind(".exe") != std::string::npos) { + // we strip .exe, only if the .exe is found + executable_name.resize(executable_name.size() - 4); + } + +#else + // Linux & OSX path delimiter + size_t delimiter_pos = executable_name.find_last_of('/'); + executable_name.erase(0, delimiter_pos + 1); +#endif + } + + // Loop over all search paths and return the first hit + for (unsigned int i = 0; i < sizeof(searchPath) / sizeof(char *); ++i) { + std::string path(searchPath[i]); + size_t executable_name_pos = path.find(""); + + // If there is executable_name variable in the searchPath + // replace it with the value + if (executable_name_pos != std::string::npos) { + if (executable_path != 0) { + path.replace(executable_name_pos, strlen(""), + executable_name); + } else { + // Skip this path entry if no executable argument is given + continue; + } + } + +#ifdef _DEBUG + printf("sdkFindFilePath <%s> in %s\n", filename, path.c_str()); +#endif + + // Test if the file exists + path.append(filename); + FILE *fp; + FOPEN(fp, path.c_str(), "rb"); + + if (fp != NULL) { + fclose(fp); + // File found + // returning an allocated array here for backwards compatibility reasons + char *file_path = reinterpret_cast(malloc(path.length() + 1)); + STRCPY(file_path, path.length() + 1, path.c_str()); + return file_path; + } + + if (fp) { + fclose(fp); + } + } + + // File not found + return 0; +} + +#endif // COMMON_HELPER_STRING_H_ diff --git a/phitest/render/cuda/src/dimensions_v2.hpp b/phitest/render/cuda/src/dimensions_v2.hpp new file mode 100644 index 0000000..b176d6b --- /dev/null +++ b/phitest/render/cuda/src/dimensions_v2.hpp @@ -0,0 +1,53 @@ + +#pragma once + +#ifndef _INCLUDE_DIMENSIONS_2 +#define _INCLUDE_DIMENSIONS_2 + +#include"vectormath.hpp" + +struct Dimensions{ + int3 input; +#ifdef CBUF_DIMENSIONS_INVERSE + float3 input_inv; +#endif + int3 output; +#ifdef CBUF_DIMENSIONS_INVERSE + float3 output_inv; +#endif +#ifdef CBUF_DIMENSIONS_BATCH + int32_t batch; +#endif +#ifdef CBUF_DIMENSIONS_CHANNEL + int32_t channel; +#endif +}; +__constant__ Dimensions c_dimensions; + +inline int3 dimensionsFromGridShape(const long long int* shape, uint32_t offset=1){ + return make_int3(shape[offset+2], shape[offset+1], shape[offset]); //default offset 1: NDHWC (zyx) -> WHD (xyz) +} + +__host__ inline void setDimensions(Dimensions& dims, const long long int* input_shape, const long long int* output_shape){ + memset(&dims, 0, sizeof(Dimensions)); + dims.input = dimensionsFromGridShape(input_shape);//swizzle from z,y,x to x,y,z + dims.output = dimensionsFromGridShape(output_shape); +#ifdef CBUF_DIMENSIONS_INVERSE + dims.input_inv = 1.f/make_float3(dims.input); + dims.output_inv = 1.f/make_float3(dims.output); +#endif +#ifdef CBUF_DIMENSIONS_BATCH + dims.batch = input_shape[0]; +#endif +#ifdef CBUF_DIMENSIONS_CHANNEL + dims.channel = input_shape[4]; +#endif + cudaError_t err = cudaMemcpyToSymbol(c_dimensions, &dims, sizeof(Dimensions)); + if(err!=cudaSuccess){ + std::cerr << "Error " << cudaGetErrorString(err) << " ("<< err << ") while setting c_dimensions constant buffer." << std::endl; + exit(1); + } +} + + +#endif //_INCLUDE_DIMENSIONS_2 \ No newline at end of file diff --git a/phitest/render/cuda/src/kernel_setup.hpp b/phitest/render/cuda/src/kernel_setup.hpp new file mode 100644 index 0000000..9cf7a80 --- /dev/null +++ b/phitest/render/cuda/src/kernel_setup.hpp @@ -0,0 +1,203 @@ + +#pragma once + +#ifndef KERNEL_SETUP +#define KERNEL_SETUP + +#define UG_PTR __restrict__ //Unique Global pointer +//#define UG_PTR + +//total maximum block size (x*y*z) is 512 (1024, depending on architecture) +//these are NOT reversed when using REVERSE_THREAD_AXIS_ORDER +#ifndef BLOCK_SIZE_X +#error "BLOCK_SIZE_X needs to be defined" +#endif + +#ifndef BLOCK_SIZE_Y +#define BLOCK_SIZE_Y 1 +#endif + +#ifndef BLOCK_SIZE_Z +#define BLOCK_SIZE_Z 1 +#endif + +#define BLOCK_SIZE BLOCK_SIZE_X*BLOCK_SIZE_Y*BLOCK_SIZE_Z +#define BLOCK_DIMS BLOCK_SIZE_X, BLOCK_SIZE_Y, BLOCK_SIZE_Z +#define MAKE_BLOCK_SIZE glm::ivec3 blockSize = glm::ivec3(BLOCK_DIMS)# + + +//https://stackoverflow.com/questions/2745074/fast-ceiling-of-an-integer-division-in-c-c +inline int32_t ceil_div(int32_t x,int32_t y){return x/y + (x%y!=0);} //(x+y-1)/y ? + +//#define REVERSE_THREAD_AXIS_ORDER +//it might be faster to reverse the thread axis order, depending on memory access patterns +#ifdef REVERSE_THREAD_AXIS_ORDER +#define GRID_DIMS(size) ceil_div(size.z, BLOCK_SIZE_X),ceil_div(size.y, BLOCK_SIZE_Y),ceil_div(size.x, BLOCK_SIZE_Z) +#define GRID_DIMS_BLOCK(size, blockSize) ceil_div(size.z, blockSize.x),ceil_div(size.y, blockSize.y),ceil_div(size.x, blockSize.z) +#else +#define GRID_DIMS(size) ceil_div(size.x, BLOCK_SIZE_X),ceil_div(size.y, BLOCK_SIZE_Y),ceil_div(size.z, BLOCK_SIZE_Z) +#define GRID_DIMS_BLOCK(size, blockSize) ceil_div(size.x, blockSize.x),ceil_div(size.y, blockSize.y),ceil_div(size.z, blockSize.z) +#endif + +//#include "render_errors.hpp" + +static void CheckCudaErrorAux(const char* file, unsigned line, const char* statement, cudaError_t err) { + if (err == cudaSuccess) return; + std::cerr << statement << " returned " << cudaGetErrorString(err) << "(" + << err << ") at " << file << ":" << line << std::endl; + exit(10); +} +#define CUDA_CHECK_RETURN(value) CheckCudaErrorAux(__FILE__, __LINE__, #value, value) +//#define CUDA_CHECK_RETURN_EXIT(value) CheckCudaErrorAux(__FILE__, __LINE__, #value, value) + +//--- Logging and Profiling --- + +#define LOG_V3_XYZ(v) "(" << v.x << "," << v.y << "," << v.z << ")" +#define LOG_V4_XYZW(v) "(" << v.x << "," << v.y << "," << v.z << "," << v.w << ")" +#define LOG_M44_COL(m) "[" << m[0][0] << "," << m[1][0] << "," << m[2][0] << "," << m[3][0] << ";\n" \ + << m[0][1] << "," << m[1][1] << "," << m[2][1] << "," << m[3][1] << ";\n" \ + << m[0][2] << "," << m[1][2] << "," << m[2][2] << "," << m[3][2] << ";\n" \ + << m[0][3] << "," << m[1][3] << "," << m[2][3] << "," << m[3][3] << "]" + +#ifdef LOG +#undef LOG +#endif +#ifdef LOGGING +#define LOG(msg) std::cout << __FILE__ << "[" << __LINE__ << "]: " << msg << std::endl +#else +#define LOG(msg) +#endif + +#ifdef PROFILING +#include +//no support for nesting for now. +auto start = std::chrono::high_resolution_clock::now(); +__host__ void beginSample(){start = std::chrono::high_resolution_clock::now();} +__host__ void endSample(std::string name){ + const auto end = std::chrono::high_resolution_clock::now(); + std::cout << "\'" << name << "\': " << (std::chrono::duration_cast(end-start).count() * 1e-6) << "ms" << std::endl; +} +#define BEGIN_SAMPLE beginSample() +#define END_SAMPLE(name) endSample(name) +#else +#define BEGIN_SAMPLE +#define END_SAMPLE(name) +#endif + +//--- Vector and Matrix helpers + +#define MAT4_FROM_ARRAY(name, arr) glm::mat4 name(arr[0], arr[1], arr[2], arr[3], arr[4], arr[5], arr[6], arr[7], arr[8], arr[9], arr[10], arr[11], arr[12], arr[13], arr[14], arr[15]) +#define IVEC3_FROM_ARRAY(name, arr) glm::ivec3 name((int32_t) (arr)[0], (int32_t) (arr)[1], (int32_t) (arr)[2]) +#define IVEC4_FROM_ARRAY(name, arr) glm::ivec4 name((int32_t) (arr)[0], (int32_t) (arr)[1], (int32_t) (arr)[2], (int32_t) (arr)[3]) +#define IVEC3_INVERT(vec) glm::ivec3(vec.z,vec.y,vec.x) //glm::swizzle(vec) + +#define EXPAND_VECTOR3(v) v.x, v.y, v.z +#define EXPAND_VECTOR3_REVERSE(v) v.z, v.y, v.x + + +//--- Index Calculations --- + +//returns the global 3D index of the current thread as vector. +__device__ inline glm::ivec3 globalThreadIdx3D(){ +#ifdef REVERSE_THREAD_AXIS_ORDER + return glm::ivec3(blockIdx.z*blockDim.z + threadIdx.z, blockIdx.y*blockDim.y + threadIdx.y, blockIdx.x*blockDim.x + threadIdx.x); +#else + return glm::ivec3(blockIdx.x*blockDim.x + threadIdx.x, blockIdx.y*blockDim.y + threadIdx.y, blockIdx.z*blockDim.z + threadIdx.z); +#endif +} +#define MAKE_GLOBAL_INDEX const glm::ivec3 globalIdx = globalThreadIdx3D() + +__device__ inline glm::ivec3 globalThreadIdx3DOverlapped(const glm::ivec3 overlap){ +#ifdef REVERSE_THREAD_AXIS_ORDER + return glm::ivec3(blockIdx.z*(blockDim.z-overlap.z) + threadIdx.z, blockIdx.y*(blockDim.y-overlap.y) + threadIdx.y, blockIdx.x*(blockDim.x-overlap.x) + threadIdx.x); +#else + return glm::ivec3(blockIdx.x*(blockDim.x-overlap.x) + threadIdx.x, blockIdx.y*(blockDim.y-overlap.y) + threadIdx.y, blockIdx.z*(blockDim.z-overlap.z) + threadIdx.z); +#endif +} + +//for bounds checking +#define CHECK_BOUNDS_SV3S(l, c1, v, c2, u) l c1 v.x && v.x c2 u && l c1 v.y && v.y c2 u && l c1 v.z && v.z c2 u +#define CHECK_BOUNDS_SV3V3(l, c1, v, c2, u) l c1 v.x && v.x c2 u.x && l c1 v.y && v.y c2 u.y && l c1 v.z && v.z c2 u.z +#define CHECK_BOUNDS_V3V3V3(l, c1, v, c2, u) l.x c1 v.x && v.x c2 u.x && l.x c1 v.y && v.y c2 u.y && l.x c1 v.z && v.z c2 u.z +#define CHECK_BOUND_SV3(v1, c, v2) v1 c v2.x && v1 c v2.y && v1 c v2.z +#define CHECK_BOUND_V3S(v1, c, v2) v1.x c v2 && v1.y c v2 && v1.z c v2 +#define CHECK_BOUND_V3V3(v1, c, v2) v1.x c v2.x && v1.y c v2.y && v1 c v2.z +template +__device__ inline bool isInDimensions(const T position, const D dimensions){ + return (position.x < dimensions.x && position.y < dimensions.y && position.z < dimensions.z); +} +template +__device__ inline bool isInDimensions(const T x, const T y, const T z, const D dimensions){ + return (x < dimensions.x && y < dimensions.y && z < dimensions.z); +} +template +__device__ inline bool isNonNegative(const V3 position){ + //return (position.x >=0 && position.y >=0 && position.z >=0); + return CHECK_BOUND_SV3(0, <=, position); +} +/* +__device__ inline bool isNonNegative(const glm::vec3 position){ + //return (position.x >=0 && position.y >=0 && position.z >=0); + return CHECK_BOUND_SV3(0.f, <=, position); +} +*/ +/* +inline __device__ __host__ bool checkBounds3D(const int3 idx, const int3 dims){ + return CHECK_BOUNDS_SV3V3(0, <=, idx, <, dims); +} +inline __device__ __host__ bool checkBounds3D(const float3 idx, const int3 dims){ + return CHECK_BOUNDS_SV3V3(0, <=, idx, <, dims); +} +inline __device__ __host__ bool checkUpperBounds3D(const int3 idx, const int3 dims){ + return CHECK_BOUNDS_V3V3(idx, <, dims); +} +*/ +//--- Common Constant Buffer Types + +#ifdef CBUF_DIMENSIONS +struct Dimensions{ + glm::ivec3 input; +#ifdef CBUF_DIMENSIONS_INVERSE + glm::vec3 input_inv; +#endif + glm::ivec3 output; +#ifdef CBUF_DIMENSIONS_INVERSE + glm::vec3 output_inv; +#endif +#ifdef CBUF_DIMENSIONS_BATCH + int32_t batch; +#endif +#ifdef CBUF_DIMENSIONS_CHANNEL + int32_t channel; +#endif +}; +__constant__ Dimensions c_dimensions; + +inline glm::ivec3 dimensionsFromGridShape(const long long int* shape, uint32_t offset=1){ + return glm::ivec3(shape[offset+2], shape[offset+1], shape[offset]); //default offset 1: NDHWC (zyx) -> WHD (xyz) +} + +__host__ inline void setDimensions(Dimensions& dims, const long long int* input_shape, const long long int* output_shape){ + memset(&dims, 0, sizeof(Dimensions)); + dims.input = dimensionsFromGridShape(input_shape);//swizzle from z,y,x to x,y,z + dims.output = dimensionsFromGridShape(output_shape); +#ifdef CBUF_DIMENSIONS_INVERSE + dims.input_inv = 1.f/glm::vec3(dims.input); + dims.output_inv = 1.f/glm::vec3(dims.output); +#endif +#ifdef CBUF_DIMENSIONS_BATCH + dims.batch = input_shape[0]; +#endif +#ifdef CBUF_DIMENSIONS_CHANNEL + dims.channel = input_shape[4]; +#endif + cudaError_t err = cudaMemcpyToSymbol(c_dimensions, &dims, sizeof(Dimensions)); + if(err!=cudaSuccess){ + std::cerr << "Error " << cudaGetErrorString(err) << " ("<< err << ") while setting c_dimensions constant buffer." << std::endl; + exit(1); + } +} +#endif //CBUF_DIMENSIONS + + +#endif //KERNEL_SETUP \ No newline at end of file diff --git a/phitest/render/cuda/src/raymarch_grid.cc b/phitest/render/cuda/src/raymarch_grid.cc new file mode 100644 index 0000000..aa444ca --- /dev/null +++ b/phitest/render/cuda/src/raymarch_grid.cc @@ -0,0 +1,539 @@ +/* +* Fused version of resample_grid and reduce_blend, no memory needed for intermediate frustum-grid +* no mip-mapping +*/ + +#include "tensorflow/core/framework/op.h" +#include "tensorflow/core/framework/op_kernel.h" +//#include "tensorflow/core/framework/array_ops.h" +#include "tensorflow/core/framework/shape_inference.h" + +#include +#include +//#define LOGGING + +#include "raymarch_grid.hpp" + +#ifdef LOGGING +#define MYLOG(msg) std::cout << msg << std::endl +#define LOG_PRINTF(msg) printf(msg) +#else +#define MYLOG(msg) +#define LOG_PRINTF(msg) +#endif + +using namespace tensorflow; + + +// Sample at transformed 3D grid positions from an input grid +REGISTER_OP("RaymarchGridTransform") + .Attr("T: {float}") + .Input("input: T") // NDHWC + .Input("matrix_m: float32") // 4x4 matrix or batch N thereof + .Input("matrix_v: float32") // 4x4 matrix or batch V thereof + .Input("matrix_p: float32") // 4x4 matrix or batch V thereof + .Input("frustum_params: float32") // 6 elemnts or batch V thereof + .Input("output_shape: int32") // DHW + .Attr("interpolation: {'NEAREST', 'LINEAR', 'MIN', 'MAX'} = 'LINEAR'") + .Attr("boundary: {'BORDER', 'CLAMP', 'WRAP'} = 'BORDER'") + //.Attr("mipmapping: {'NONE', 'NEAREST', 'LINEAR'} = 'NONE'") + //.Attr("num_mipmaps: int = 0") + //.Attr("mip_bias: float = 0.0") + //.Attr("coordinate_mode: {'TRANSFORM_LINDEPTH', 'TRANSFORM_LINDEPTH_REVERSE', 'TRANSFORM', 'TRANSFORM_REVERSE'} = 'TRANSFORM_LINDEPTH'") + //.Attr("cell_center_offset: float = 0.5") // offset of cell center coordinates from cell indices. should be 0.5 for all transform modes and if mipmapping is used + .Attr("blending_mode: {'BEER_LAMBERT', 'ALPHA', 'ALPHA_ADDITIVE', 'ADDITIVE'} = 'BEER_LAMBERT'") + .Attr("keep_dims: bool = false") + .Attr("separate_camera_batch: bool = true") + .Output("output: T") // NVDHWC + .SetShapeFn([](::tensorflow::shape_inference::InferenceContext* c) + { + ::tensorflow::shape_inference::ShapeHandle batch; + TF_RETURN_IF_ERROR(c->Subshape(c->input(0), 0, &batch)); + + ::tensorflow::shape_inference::ShapeHandle outShape; + TF_RETURN_IF_ERROR(c->MakeShapeFromShapeTensor(3, &outShape)); + TF_RETURN_IF_ERROR(c->Subshape(outShape, 0, 3, &outShape)); + TF_RETURN_IF_ERROR(c->Concatenate(batch, outShape, &outShape)); + + ::tensorflow::shape_inference::ShapeHandle channel; + TF_RETURN_IF_ERROR(c->Subshape(c->input(0), 4, &channel)); + TF_RETURN_IF_ERROR(c->Concatenate(outShape, channel, &outShape)); + c->set_output(0, outShape); + return Status::OK(); + }); + + +// the gradient op +REGISTER_OP("RaymarchGridTransformGrad") + .Attr("T: {float}") + .Input("input: T") // NDHWC + .Input("output: T") // NVHWC + .Input("output_grad: T") // NVHWC + .Input("matrix_m: float32") // 4x4 matrix or batch N thereof + .Input("matrix_v: float32") // 4x4 matrix or batch V thereof + .Input("matrix_p: float32") // 4x4 matrix or batch V thereof + .Input("frustum_params: float32") + .Input("output_shape: int32") // DHW + .Attr("interpolation: {'LINEAR'} = 'LINEAR'") //TODO 'NEAREST', 'MIN', 'MAX' + .Attr("boundary: {'BORDER', 'CLAMP', 'WRAP'} = 'BORDER'") + //.Attr("mipmapping: {'NONE'} = 'NONE'") //, 'NEAREST', 'LINEAR'. currently not supported + //.Attr("num_mipmaps: int = 0") + //.Attr("mip_bias: float = 0.0") + //.Attr("coordinate_mode: {'TRANSFORM_LINDEPTH', 'TRANSFORM_LINDEPTH_REVERSE', 'TRANSFORM', 'TRANSFORM_REVERSE'} = 'TRANSFORM_LINDEPTH'") //, 'RAY' + //.Attr("cell_center_offset: float = 0.5") // offset of cell center coordinates from cell indices. should be 0.5 for all transform modes and if mipmapping is used + .Attr("blending_mode: {'BEER_LAMBERT', 'ALPHA', 'ALPHA_ADDITIVE', 'ADDITIVE'} = 'BEER_LAMBERT'") + .Attr("keep_dims: bool = false") + .Attr("separate_camera_batch: bool = true") + .Output("input_grad: T") + .SetShapeFn([](::tensorflow::shape_inference::InferenceContext* c) + { + c->set_output(0, c->input(0)); + return Status::OK(); + }); + +//TODO: this fused version could support raymarching from a 2D pixel grid of starting positions, directions and step size + + + +Status makeShapeFromTensor(const Tensor& sizes, TensorShape* shape){ + MYLOG("Create shape from tensor"); + auto sizes_flat = sizes.flat(); + const int64 num_dims = sizes_flat.size(); + for(int64 i=0; iAddDim(dim); + } + return Status::OK(); +} + +bool isValidTransformMatrix(const TensorShape& matrixShape, int32_t& numMatrices){ + if(matrixShape.dims()==2){ + numMatrices = 1; + return matrixShape.dim_size(0)==matrixShape.dim_size(1) && matrixShape.dim_size(0)==4; + } else if(matrixShape.dims()==3){ + numMatrices = matrixShape.dim_size(0); + return matrixShape.dim_size(1)==matrixShape.dim_size(2) && matrixShape.dim_size(1)==4; + } else{ + numMatrices = 0; + return false; + } +} + +bool isValidFrustumParams(const TensorShape& shape, int32_t& numCameras){ + if(shape.dims()==1){ + numCameras = 1; + return shape.dim_size(0)==6; + }else if(shape.dims()==2){ + numCameras = shape.dim_size(0); + return shape.dim_size(1)==6; + }else{ + numCameras = 0; + return false; + } +} + +bool parseFilterMode(OpKernelConstruction *context, const std::string& filterModeStr, Sampling::FilterMode& filterMode){ + if(filterModeStr.compare("NEAREST")==0) filterMode = Sampling::FILTERMODE_NEAREST; + else if(filterModeStr.compare("LINEAR")==0) filterMode = Sampling::FILTERMODE_LINEAR; + else if(filterModeStr.compare("MIN")==0) filterMode = Sampling::FILTERMODE_MIN; + else if(filterModeStr.compare("MAX")==0) filterMode = Sampling::FILTERMODE_MAX; + else return false; + return true; +} + +bool parseBoundaryMode(OpKernelConstruction *context, const std::string& boundaryModeStr, Sampling::BoundaryMode& boundaryMode){ + if(boundaryModeStr.compare("BORDER")==0) boundaryMode = Sampling::BOUNDARY_BORDER; + else if(boundaryModeStr.compare("CLAMP")==0) boundaryMode = Sampling::BOUNDARY_CLAMP; + else if(boundaryModeStr.compare("WRAP")==0) boundaryMode = Sampling::BOUNDARY_WRAP; + //else if(boundaryModeStr.compare("MIRROR")==0) boundaryMode = Sampling::BOUNDARY_MIRROR; + else return false; + return true; +} + +bool parseBlendMode(OpKernelConstruction *context, const std::string& blendModeStr, Blending::BlendMode& blendMode){ + if(blendModeStr.compare("BEER_LAMBERT")==0) blendMode = Blending::BLEND_BEERLAMBERT; + else if(blendModeStr.compare("ALPHA")==0) blendMode = Blending::BLEND_ALPHA; + else if(blendModeStr.compare("ALPHA_ADDITIVE")==0) blendMode = Blending::BLEND_ALPHAADDITIVE; + else if(blendModeStr.compare("ADDITIVE")==0) blendMode = Blending::BLEND_ADDITIVE; + else return false; + return true; +} + +#if GOOGLE_CUDA +#define DECLARE_GPU_SPEC(T, C) \ + template<> \ + void RaymarchGridKernel::operator()( \ + const GPUDevice& d, \ + const T* input, const long long int* input_shape, \ + const float* M, const float* V, const float* P, const float* frustum, int32_t numCameras, \ + const Sampling::FilterMode filterMode, const Sampling::BoundaryMode boundaryMode, \ + const Blending::BlendMode blendMode, const bool globalSampling, \ + T* output, const long long int* output_shape \ + ); \ + extern template struct RaymarchGridKernel; +DECLARE_GPU_SPEC(float, 1) +DECLARE_GPU_SPEC(float, 2) +DECLARE_GPU_SPEC(float, 4) +#undef DECLARE_GPU_SPEC +#endif + +//only for 3D grids with up to 4 channel +//no support for batches yet? +template +class RaymarchGridTransformOp : public OpKernel{ +public: + explicit RaymarchGridTransformOp(OpKernelConstruction *context) : OpKernel(context){ + + std::string s_interpolation; + OP_REQUIRES_OK(context, context->GetAttr("interpolation", &s_interpolation)); + OP_REQUIRES(context, parseFilterMode(context, s_interpolation, m_filterMode), errors::InvalidArgument("invalid filter mode")); + + std::string s_boundary; + OP_REQUIRES_OK(context, context->GetAttr("boundary", &s_boundary)); + OP_REQUIRES(context, parseBoundaryMode(context, s_boundary, m_boundaryMode), errors::InvalidArgument("invalid boundary mode")); + + OP_REQUIRES_OK(context, context->GetAttr("separate_camera_batch", &m_globalSampling)); + + std::string blendingMode; + OP_REQUIRES_OK(context, context->GetAttr("blending_mode", &blendingMode)); + OP_REQUIRES(context, parseBlendMode(context, blendingMode, m_blendMode), errors::InvalidArgument("invalid blend mode")); + + OP_REQUIRES_OK(context, context->GetAttr("keep_dims", &m_keepDims)); + } + + void Compute(OpKernelContext *context) override{ + + MYLOG("RaymarchGridTransformOp kernel start"); + + const Tensor& input_grid = context->input(0); + const Tensor& tensor_M = context->input(1); + const Tensor& tensor_V = context->input(2); + const Tensor& tensor_P = context->input(3); + const Tensor& frustum = context->input(4); + const Tensor& output_shape_tensor = context->input(5); + + //check input + MYLOG("Check input"); + TensorShape input_shape = input_grid.shape(); + OP_REQUIRES(context, input_grid.dims()==5 && input_shape.dim_size(4)<=4, + errors::InvalidArgument("Invalid input shape (NDHWC):", input_shape.DebugString())); + const int64 batch = input_shape.dim_size(0); + const int64 channel = input_shape.dim_size(4); + + //check output shape + MYLOG("Check output shape tensor"); + OP_REQUIRES(context, output_shape_tensor.dims()==1 && output_shape_tensor.dim_size(0)==3, errors::InvalidArgument("Invalid output_shape")); + MYLOG("Create output shape"); + TensorShape output_shape; + OP_REQUIRES_OK(context, makeShapeFromTensor(output_shape_tensor, &output_shape)); + MYLOG("Check output shape"); + OP_REQUIRES(context, output_shape.dims()==3, + errors::InvalidArgument("Invalid output_shape")); + //const int64 depthSamples = output_shape.dim_size(0); + output_shape.InsertDim(0,batch); + output_shape.AddDim(channel); + MYLOG("output_shape: " << output_shape.dim_size(0) << ", " << output_shape.dim_size(1) << ", " << output_shape.dim_size(2) << ", " << output_shape.dim_size(3)); + + //check transform matrics + int32_t numCameras=0; + MYLOG("Check transform"); + int32 numMatrix_M=0, numMatrix_V=0, numMatrix_P=0, numFrustum=0; + OP_REQUIRES(context, isValidTransformMatrix(tensor_M.shape(), numMatrix_M), + errors::InvalidArgument("transformation matrix_m must be a 4x4 square matrix or a batch of 4x4 square matrices:", tensor_M.shape().DebugString())); + OP_REQUIRES(context, numMatrix_M==batch, errors::InvalidArgument("model matrix batch size mismatch")); + + OP_REQUIRES(context, isValidTransformMatrix(tensor_V.shape(), numMatrix_V), + errors::InvalidArgument("transformation matrix_v must be a 4x4 square matrix or a batch of 4x4 square matrices:", tensor_V.shape().DebugString())); + OP_REQUIRES(context, isValidTransformMatrix(tensor_P.shape(), numMatrix_P), + errors::InvalidArgument("transformation matrix_p must be a 4x4 square matrix or a batch of 4x4 square matrices:", tensor_P.shape().DebugString())); + OP_REQUIRES(context, isValidFrustumParams(frustum.shape(), numFrustum), + errors::InvalidArgument("frustum must be a 1D tensor of 6 elements or a batch thereof:", frustum.shape().DebugString())); + //TODO + OP_REQUIRES(context, numMatrix_V==numMatrix_P && numMatrix_V==numFrustum, errors::InvalidArgument("camera batch size mismatch")); + numCameras = numMatrix_V; + + if(!m_globalSampling){ + OP_REQUIRES(context, numCameras==batch, errors::InvalidArgument("camera batch must match data batch when not using global sampling.")); + output_shape.InsertDim(1,1); + }else{ + output_shape.InsertDim(1,numCameras); + } + + + + //allocate outout + MYLOG("Allocate output"); + //OP_REQUIRES(context, output_shape.IsValid(), errors::InvalidArgument("Broken output shape")); + //if(!m_keepDims) output_shape.set_dim(0,1); + Tensor* output_grid = NULL; + TensorShape output_tensor_shape = output_shape; + if(m_keepDims){ //return NVDHWC with D=1 + output_tensor_shape.set_dim(2,1); + }else{ //return NVHWC + output_tensor_shape.RemoveDim(2); + } + OP_REQUIRES_OK(context, context->allocate_output(0, output_tensor_shape, &output_grid)); + MYLOG("Check allocated output\n"); + + + + MYLOG("Resample"); + switch(channel){ + case 1: + RaymarchGridKernel()(context->eigen_device(), + input_grid.flat().data(), input_shape.dim_sizes().data(), + tensor_M.flat().data(), tensor_V.flat().data(), tensor_P.flat().data(), frustum.flat().data(), numCameras, + m_filterMode, m_boundaryMode, m_blendMode, m_globalSampling, + output_grid->flat().data(), output_shape.dim_sizes().data()); + break; + case 2: + RaymarchGridKernel()(context->eigen_device(), + input_grid.flat().data(), input_shape.dim_sizes().data(), + tensor_M.flat().data(), tensor_V.flat().data(), tensor_P.flat().data(), frustum.flat().data(), numCameras, + m_filterMode, m_boundaryMode, m_blendMode, m_globalSampling, + output_grid->flat().data(), output_shape.dim_sizes().data()); + break; + case 4: + RaymarchGridKernel()(context->eigen_device(), + input_grid.flat().data(), input_shape.dim_sizes().data(), + tensor_M.flat().data(), tensor_V.flat().data(), tensor_P.flat().data(), frustum.flat().data(), numCameras, + m_filterMode, m_boundaryMode, m_blendMode, m_globalSampling, + output_grid->flat().data(), output_shape.dim_sizes().data()); + break; + default: + OP_REQUIRES(context, false, + errors::Unimplemented("Only 1,2 and 4 Channel supported.")); + } + + MYLOG("RaymarchGridTransformOp kernel done"); + } +private: + bool m_globalSampling; + bool m_keepDims; + Sampling::FilterMode m_filterMode; + Sampling::BoundaryMode m_boundaryMode; + Blending::BlendMode m_blendMode; +}; + + +#if GOOGLE_CUDA +#define DECLARE_GPU_SPEC(T, C) \ + template<> \ + void RaymarchGridGradKernel::operator()( \ + const GPUDevice& d, \ + const T* input, T* inputGrads, T* sampleBuffer, sampleCount_t* sampleCounter, const long long int* input_shape, \ + const float* M, const float* V, const float* P, const float* frustum, int32_t numCameras, \ + const Sampling::FilterMode filterMode, const Sampling::BoundaryMode boundaryMode, \ + const Blending::BlendMode blendMode, const bool globalSampling, \ + const T* output, const T* outputGrads, const long long int* output_shape \ + ); \ + extern template struct RaymarchGridGradKernel; +DECLARE_GPU_SPEC(float, 1) +DECLARE_GPU_SPEC(float, 2) +DECLARE_GPU_SPEC(float, 4) +#undef DECLARE_GPU_SPEC +#endif + +template +class RaymarchGridTransformGradOp : public OpKernel{ +public: + explicit RaymarchGridTransformGradOp(OpKernelConstruction *context) : OpKernel(context){ + + std::string s_interpolation; + OP_REQUIRES_OK(context, context->GetAttr("interpolation", &s_interpolation)); + OP_REQUIRES(context, parseFilterMode(context, s_interpolation, m_filterMode), errors::InvalidArgument("invalid filter mode")); + + std::string s_boundary; + OP_REQUIRES_OK(context, context->GetAttr("boundary", &s_boundary)); + OP_REQUIRES(context, parseBoundaryMode(context, s_boundary, m_boundaryMode), errors::InvalidArgument("invalid boundary mode")); + + OP_REQUIRES_OK(context, context->GetAttr("separate_camera_batch", &m_globalSampling)); + + std::string blendingMode; + OP_REQUIRES_OK(context, context->GetAttr("blending_mode", &blendingMode)); + OP_REQUIRES(context, parseBlendMode(context, blendingMode, m_blendMode), errors::InvalidArgument("invalid blend mode")); + + OP_REQUIRES_OK(context, context->GetAttr("keep_dims", &m_keepDims)); + } + + void Compute(OpKernelContext *context) override{ + + MYLOG("RaymarchGridTransformGradOp kernel start"); + + const Tensor& input_grid = context->input(0); + const Tensor& output_grid = context->input(1); + const Tensor& output_grad_grid = context->input(2); + const Tensor& tensor_M = context->input(3); + const Tensor& tensor_V = context->input(4); + const Tensor& tensor_P = context->input(5); + const Tensor& frustum = context->input(6); + //still needed for number of samples in depth + const Tensor& output_shape_tensor = context->input(7); + + //check input + MYLOG("Check input"); + TensorShape input_shape = input_grid.shape(); + OP_REQUIRES(context, input_grid.dims()==5 && input_shape.dim_size(4)<=4, + errors::InvalidArgument("Invalid input shape (NDHWC):", input_shape.DebugString())); + const int64 batch = input_shape.dim_size(0); + const int64 channel = input_shape.dim_size(4); + + //check output gradients + MYLOG("Check output_grads"); + TensorShape output_shape = output_grid.shape(); + OP_REQUIRES(context, output_shape==output_grad_grid.shape(), + errors::InvalidArgument("Shapes of output grid and output gradients must match:", output_shape.DebugString(), output_grad_grid.shape().DebugString())); + + MYLOG("output_shape: " << output_shape.dim_size(0) << ", " << output_shape.dim_size(1) << ", " << output_shape.dim_size(2) << ", " << output_shape.dim_size(3)); + if(m_keepDims){ + OP_REQUIRES(context, output_grad_grid.dims()==6 && output_shape.dim_size(5)<=4, + errors::InvalidArgument("Invalid output_grads shape (NVDHWC):", output_shape.DebugString())); + }else{ + OP_REQUIRES(context, output_grad_grid.dims()==5 && output_shape.dim_size(4)<=4, + errors::InvalidArgument("Invalid output_grads shape (NVHWC):", output_shape.DebugString())); + output_shape.InsertDim(2,1); //make NVDHWC for internal use + } + OP_REQUIRES(context, output_shape.dim_size(0)==batch, + errors::InvalidArgument("output_grads batch size does not match input batch size:", output_shape.dim_size(0), batch)); + + + + //check transform matrics + int32_t numCameras=0; + MYLOG("Check transform"); + int32 numMatrix_M=0, numMatrix_V=0, numMatrix_P=0, numFrustum=0; + OP_REQUIRES(context, isValidTransformMatrix(tensor_M.shape(), numMatrix_M), + errors::InvalidArgument("transformation matrix_m must be a 4x4 square matrix or a batch of 4x4 square matrices:", tensor_M.shape().DebugString())); + OP_REQUIRES(context, numMatrix_M==batch, errors::InvalidArgument("model matrix batch size mismatch")); + + OP_REQUIRES(context, isValidTransformMatrix(tensor_V.shape(), numMatrix_V), + errors::InvalidArgument("transformation matrix_v must be a 4x4 square matrix or a batch of 4x4 square matrices:", tensor_V.shape().DebugString())); + OP_REQUIRES(context, isValidTransformMatrix(tensor_P.shape(), numMatrix_P), + errors::InvalidArgument("transformation matrix_p must be a 4x4 square matrix or a batch of 4x4 square matrices:", tensor_P.shape().DebugString())); + OP_REQUIRES(context, isValidFrustumParams(frustum.shape(), numFrustum), + errors::InvalidArgument("frustum must be a 1D tensor of 6 elements or a batch thereof:", frustum.shape().DebugString())); + //TODO + OP_REQUIRES(context, numMatrix_V==numMatrix_P && numMatrix_V==numFrustum, errors::InvalidArgument("camera batch size mismatch")); + numCameras = numMatrix_V; + if(!m_globalSampling){ + OP_REQUIRES(context, numCameras==batch, errors::InvalidArgument("camera batch must match data batch when not using global sampling.")); + + OP_REQUIRES(context, output_shape.dim_size(1)==1, + errors::InvalidArgument("output_grads view dimension must be 1 if not using global sampling.")); + }else{ + OP_REQUIRES(context, output_shape.dim_size(1)==numCameras, + errors::InvalidArgument("output_grads views dimension does not match number of cameras.")); + } + + TensorShape orig_output_shape; //NVDHWC matching output, but D is the number of depth samples from the original output_shape + OP_REQUIRES_OK(context, makeShapeFromTensor(output_shape_tensor, &orig_output_shape)); + MYLOG("Check output shape"); + OP_REQUIRES(context, orig_output_shape.dims()==3 && output_shape.dim_size(3)==orig_output_shape.dim_size(1) && output_shape.dim_size(4)==orig_output_shape.dim_size(2), + errors::InvalidArgument("output_shape must be rank 3 (DHW) with H and W matching those of output (NVDHWC):", orig_output_shape.DebugString(), output_shape.DebugString())); + //const int64 depthSamples = output_shape.dim_size(0); + orig_output_shape.InsertDim(0,batch); + orig_output_shape.InsertDim(1, output_shape.dim_size(1)); + orig_output_shape.AddDim(channel); + + sampleCount_t* sample_count_buffer = nullptr; + T* sample_buffer = nullptr; + Tensor sample_count_tensor; + Tensor sample_buffer_tensor; + if(NORMALIZE_GRADIENTS!=NORMALIZE_GRADIENT_NONE){ + MYLOG("Allocate gradient counter"); + TensorShape sample_count_tensor_shape; + sample_count_tensor_shape.AddDim(input_shape.dim_size(1)); + sample_count_tensor_shape.AddDim(input_shape.dim_size(2)); + sample_count_tensor_shape.AddDim(input_shape.dim_size(3)); + OP_REQUIRES_OK(context, context->allocate_temp(TFsampleCount_t, sample_count_tensor_shape, &sample_count_tensor)); + sample_count_buffer = sample_count_tensor.flat().data(); + if(numCameras>1 && m_globalSampling){ + sample_count_tensor_shape.AddDim(channel); + sample_count_tensor_shape.AddDim(sizeof(T)); + OP_REQUIRES_OK(context, context->allocate_temp(DataTypeToEnum::value, sample_count_tensor_shape, &sample_buffer_tensor)); + sample_buffer = sample_buffer_tensor.flat().data(); + } + } + + //allocate outout + MYLOG("Allocate input gradients"); + Tensor* input_grads = nullptr; + OP_REQUIRES_OK(context, context->allocate_output(0, input_shape, &input_grads)); + + + MYLOG("Resample\n"); + switch(channel){ + case 1: + RaymarchGridGradKernel()(context->eigen_device(), + input_grid.flat().data(), input_grads->flat().data(), sample_buffer, sample_count_buffer, input_shape.dim_sizes().data(), + tensor_M.flat().data(), tensor_V.flat().data(), tensor_P.flat().data(), frustum.flat().data(), numCameras, + m_filterMode, m_boundaryMode, m_blendMode, m_globalSampling, + output_grid.flat().data(), output_grad_grid.flat().data(), orig_output_shape.dim_sizes().data()); + break; + case 2: + RaymarchGridGradKernel()(context->eigen_device(), + input_grid.flat().data(), input_grads->flat().data(), sample_buffer, sample_count_buffer, input_shape.dim_sizes().data(), + tensor_M.flat().data(), tensor_V.flat().data(), tensor_P.flat().data(), frustum.flat().data(), numCameras, + m_filterMode, m_boundaryMode, m_blendMode, m_globalSampling, + output_grid.flat().data(), output_grad_grid.flat().data(), orig_output_shape.dim_sizes().data()); + break; + case 4: + RaymarchGridGradKernel()(context->eigen_device(), + input_grid.flat().data(), input_grads->flat().data(), sample_buffer, sample_count_buffer, input_shape.dim_sizes().data(), + tensor_M.flat().data(), tensor_V.flat().data(), tensor_P.flat().data(), frustum.flat().data(), numCameras, + m_filterMode, m_boundaryMode, m_blendMode, m_globalSampling, + output_grid.flat().data(), output_grad_grid.flat().data(), orig_output_shape.dim_sizes().data()); + break; + default: + OP_REQUIRES(context, false, + errors::Unimplemented("Only 1,2 and 4 Channel supported.")); + } + //*/ + MYLOG("RaymarchGridTransformGradOp kernel done"); + } +private: + bool m_globalSampling; + bool m_keepDims; + Sampling::FilterMode m_filterMode; + Sampling::BoundaryMode m_boundaryMode; + Blending::BlendMode m_blendMode; +}; + + + +#define REGISTER__CPU(T) + +#undef REGISTER__CPU + +#if GOOGLE_CUDA +#define REGISTER_GPU(T) \ + REGISTER_KERNEL_BUILDER(Name("RaymarchGridTransform") \ + .Device(DEVICE_GPU) \ + .TypeConstraint("T") \ + .HostMemory("matrix_m") \ + .HostMemory("matrix_v") \ + .HostMemory("matrix_p") \ + .HostMemory("frustum_params") \ + .HostMemory("output_shape") \ + , RaymarchGridTransformOp); +REGISTER_GPU(float); +#undef REGISTER_GPU + +#define REGISTER_GPU(T) \ + REGISTER_KERNEL_BUILDER(Name("RaymarchGridTransformGrad") \ + .Device(DEVICE_GPU) \ + .TypeConstraint("T") \ + .HostMemory("matrix_m") \ + .HostMemory("matrix_v") \ + .HostMemory("matrix_p") \ + .HostMemory("frustum_params") \ + .HostMemory("output_shape") \ + , RaymarchGridTransformGradOp); +REGISTER_GPU(float); +#undef REGISTER_GPU + +#endif //GOOGLE_CUDA \ No newline at end of file diff --git a/phitest/render/cuda/src/raymarch_grid.cu.cc b/phitest/render/cuda/src/raymarch_grid.cu.cc new file mode 100644 index 0000000..b6e3d6e --- /dev/null +++ b/phitest/render/cuda/src/raymarch_grid.cu.cc @@ -0,0 +1,748 @@ +/* helpers */ + +#include +#include "cuda-samples/Common/helper_cuda.h" +#include +#include + +static void CheckCudaErrorAux(const char* file, unsigned line, const char* statement, cudaError_t err) { + if (err == cudaSuccess) return; + std::cerr << statement << " returned " << cudaGetErrorString(err) << "(" + << err << ") at " << file << ":" << line << std::endl; + exit(10); +} +#define CUDA_CHECK_RETURN(value) CheckCudaErrorAux(__FILE__, __LINE__, #value, value) +//#define CUDA_CHECK_RETURN_EXIT(value) CheckCudaErrorAux(__FILE__, __LINE__, #value, value) + +//--- Logging and Profiling --- +//#define LOGGING +#define LOG_V3_XYZ(v) "(" << v.x << "," << v.y << "," << v.z << ")" +#define LOG_V4_XYZW(v) "(" << v.x << "," << v.y << "," << v.z << "," << v.w << ")" +#define LOG_M44_COL(m) "[" << m[0][0] << "," << m[1][0] << "," << m[2][0] << "," << m[3][0] << ";\n" \ + << m[0][1] << "," << m[1][1] << "," << m[2][1] << "," << m[3][1] << ";\n" \ + << m[0][2] << "," << m[1][2] << "," << m[2][2] << "," << m[3][2] << ";\n" \ + << m[0][3] << "," << m[1][3] << "," << m[2][3] << "," << m[3][3] << "]" + +#ifdef LOG +#undef LOG +#endif +#ifdef LOGGING +#define LOG(msg) std::cout << __FILE__ << "[" << __LINE__ << "]: " << msg << std::endl +#else +#define LOG(msg) +#endif + +#ifdef PROFILING +#include +//no support for nesting for now. +auto start = std::chrono::high_resolution_clock::now(); +__host__ void beginSample(){start = std::chrono::high_resolution_clock::now();} +__host__ void endSample(std::string name){ + const auto end = std::chrono::high_resolution_clock::now(); + std::cout << "\'" << name << "\': " << (std::chrono::duration_cast(end-start).count() * 1e-6) << "ms" << std::endl; +} +#define BEGIN_SAMPLE beginSample() +#define END_SAMPLE(name) endSample(name) +#else +#define BEGIN_SAMPLE +#define END_SAMPLE(name) +#endif + +//total maximum block size (x*y*z) is 512 (1024, depending on architecture) +//these are NOT reversed when using REVERSE_THREAD_AXIS_ORDER +#define BLOCK_SIZE_X 16 +#define BLOCK_SIZE_Y 8 +#define BLOCK_SIZE_Z 1 + +#define BLOCK_SIZE BLOCK_SIZE_X*BLOCK_SIZE_Y*BLOCK_SIZE_Z +#define BLOCK_DIMS BLOCK_SIZE_X, BLOCK_SIZE_Y, BLOCK_SIZE_Z + +#include"raymarch_grid.hpp" +#include"vectormath.hpp" + +#define CBUF_FRUSTUM +#define CBUF_TRANSFORM_INVERSE +#define CBUF_TRANSFORM_NORMAL +#include"transformations_v2.hpp" + +#define CBUF_DIMENSIONS_INVERSE +#include"dimensions_v2.hpp" + +#include"sampling_v2.hpp" +//#include"sampling_settings_v2.hpp" +#include"blending.hpp" +//#include"blending_settings.hpp" +#include"vector_io.hpp" + +//https://stackoverflow.com/questions/2745074/fast-ceiling-of-an-integer-division-in-c-c +inline int32_t ceil_div(int32_t x,int32_t y){return (x+y-1)/y;} +__host__ inline dim3 gridDims3D(const int3 dimensions){ + return dim3(ceil_div(dimensions.x, BLOCK_SIZE_X),ceil_div(dimensions.y, BLOCK_SIZE_Y),ceil_div(dimensions.z, BLOCK_SIZE_Z)); +} +__host__ inline dim3 blockDims3D(){ + return dim3(BLOCK_SIZE_X,BLOCK_SIZE_Y,BLOCK_SIZE_Z); +} + +//returns the global 3D index of the current thread as vector. +__device__ inline int3 globalThreadIdx3D(){ + return make_int3(blockIdx.x*blockDim.x + threadIdx.x, blockIdx.y*blockDim.y + threadIdx.y, blockIdx.z*blockDim.z + threadIdx.z); +} + +template +__device__ inline bool isInDimensions(const T position, const D dimensions){ + return (position.x < dimensions.x && position.y < dimensions.y && position.z < dimensions.z); +} +template +__device__ inline bool isInDimensions(const T x, const T y, const T z, const D dimensions){ + return (x < dimensions.x && y < dimensions.y && z < dimensions.z); +} + +template +__device__ inline T getColorFromPos(const float3 pos); +template<> +__device__ inline float1 getColorFromPos(const float3 pos){return make_float1(pos.z);} +template<> +__device__ inline float2 getColorFromPos(const float3 pos){return make_float2(pos);} +template<> +__device__ inline float4 getColorFromPos(const float3 pos){return make_float4(pos,1.f);} + +__device__ inline float2 RayGridIntersectionDistances(const float3 origin, const float3 direction, const float3 borderOffset){ + /* + * Return the entry and exit distances to the intersection of the grid's boundary box with the ray defined by origin and direction. + * origin and direction are in the grid's object space (grid starts at (0,0,0) and each cell has size (1,1,1)), direction must be normalized. + * the grid dimensions are given by c_dimensions.input. + * borderOffset adjusts the bounding box used for intersection. + * - at 0 the bounding box is aligned with the outer cell borders. + * - at -0.5 the bounding box is aligned with the cell centers of the other cells. + * + * https://github.com/erich666/GraphicsGems/blob/master/gems/RayBox.c + * https://www.iquilezles.org/www/articles/intersectors/intersectors.htm + * https://iquilezles.org/www/articles/boxfunctions/boxfunctions.htm + * + * https://people.csail.mit.edu/amy/papers/box-jgt.pdf + * https://medium.com/@bromanz/another-view-on-the-classic-ray-aabb-intersection-algorithm-for-bvh-traversal-41125138b525 + */ + // Box: Grid is from 0 to c_dimensions.input in object space + // bounds[0] is always 0, bounds[1] is c_dimensions.input + //const float3 boundsMin = make_float3(0.0f); + const float3 boundsMax = make_float3(c_dimensions.input); + float3 halfSize = boundsMax * 0.5f + borderOffset; //from center to outer cell border + + // original uses origin-centered box, so shift ray origin + const float3 centeredOrigin = origin - boundsMax * 0.5f; + + // make bounds slightly smaller to start rendering after passing the first cell center values (0.5 cells from the domain border) to avoid boundary artifacts. + // ray shift still needs to use the actual domain size or only one side in each dimension will be cut. + //boundsMax = boundsMax - 0.2f; + //const float3 halfSize = (boundsMax - 0.2f) * 0.5f; + /* if(BM==Sampling::BOUNDARY_BORDER){ + halfSize += 0.5f; // + to start outside/at first layer of ghost cell centers, used with border boundary mode and constant 1 around domain. + } else { + halfSize -= 0.6f; + } */ + + const float3 invDir = 1.0f / direction; + // const float3 t0 = (boundsMin - origin) * invDir; + // const float3 t1 = (boundsMax - origin) * invDir; + const float3 n = invDir * centeredOrigin; + const float3 k = fabs(invDir) * (halfSize); + const float3 tsmaller = 0.0f - n - k; //t1=-n-k + const float3 tbigger = k - n; //t2=-n+k + + float tmin = fmaxf(fmaxf(tsmaller.x, tsmaller.y), tsmaller.z); + float tmax = fminf(fminf(tbigger.x, tbigger.y), tbigger.z); + + return make_float2(tmin, tmax); +} +template +__device__ inline float2 RayGridIntersectionDistances(const float3 origin, const float3 direction){ + //compatibility + if(BM==Sampling::BOUNDARY_BORDER){ + return RayGridIntersectionDistances(origin, direction, make_float3(0.5f)); // + to start outside/at first layer of ghost cell centers, used with border boundary mode and constant 1 around domain. + } else { + return RayGridIntersectionDistances(origin, direction, make_float3(-0.6f)); + } +} + +template +__device__ inline bool CheckRayGridIntersection(const float3 origin, const float3 direction){ + float2 t = RayGridIntersectionDistances(origin, direction); + return t.x<=t.y && t.y>=0; +} + +template +__device__ inline bool getRaymarchStep(const int3 globalIdx, float3 & startPos, float3 & endPos, float3 & step, int32_t & steps){ + const float zMax = static_cast(max(c_dimensions.output.z-1, 1)); + startPos = make_float3(IDXtoOS(indexToCoords(make_float3(globalIdx.x, globalIdx.y, 0.f)), c_dimensions.output_inv)); + endPos = make_float3(IDXtoOS(indexToCoords(make_float3(globalIdx.x, globalIdx.y, zMax)), c_dimensions.output_inv)); + + step = (endPos - startPos) / zMax; + steps = c_dimensions.output.z; + + if(BM==Sampling::BOUNDARY_BORDER){ + // outside of boundary is ignored, so don't need to actually sample there + //const float3 ray = startPos - endPos; + const float3 rayDir = normalize(endPos - startPos); + // check ray against grid AABB -> start and end points/distance + const float2 AABBdistance = RayGridIntersectionDistances(startPos, rayDir, make_float3(-0.5f)); + if(AABBdistance.x>AABBdistance.y || AABBdistance.y<0){ //(!(AABBdistance.x<=AABBdistance.y && AABBdistance.y>=0)) + //ray does not hit the domain + return false; + } + endPos = startPos + rayDir * (AABBdistance.y + 1.f); + startPos = startPos + rayDir * max((AABBdistance.x - 1.f), 0.f); + const float stepSize = length(step); + const float rayLength = length(startPos - endPos); + steps = static_cast(rayLength / stepSize) + 2; + } + + return true; +} + +template +__global__ void +__launch_bounds__(BLOCK_SIZE) +kRaymarchGridTransformLindepth(const T* input, T* output){ + /* front to back. get first and last sample point and interpolate for linear depth. + * output: 3D array DHW with D=1 + * outputDimensions: dimensions of output with z number of steps + */ + const int3 globalIdx = globalThreadIdx3D(); +#ifdef LOGGING + if(globalIdx.x==0&& globalIdx.y==0&& globalIdx.z==0){ + printf("--- Kernel running! ---\n"); + printf("K: zMax = %f\n", static_cast(max(c_dimensions.output.z-1, 1))); + } +#endif + if(globalIdx.x(max(c_dimensions.output.z-1, 1)); + const float3 nearPos = make_float3(IDXtoOS(indexToCoords(make_float3(globalIdx.x, globalIdx.y, 0.f)), c_dimensions.output_inv)); + const float3 farPos = make_float3(IDXtoOS(indexToCoords(make_float3(globalIdx.x, globalIdx.y, zMax)), c_dimensions.output_inv)); + + const float3 step = (farPos - nearPos) / zMax; + int32_t steps = c_dimensions.output.z; + */ + float3 nearPos; + float3 farPos; + float3 step; + int32_t steps; + const bool hit = getRaymarchStep(globalIdx, nearPos, farPos, step, steps); + + /* + T acc = getColorFromPos(make_float3( + globalIdx.x * c_dimensions.output_inv.x, + globalIdx.y * c_dimensions.output_inv.y, + zMax + ));*/ + + T acc = vmath::make_cudaFloat(0.f); + if(hit){ + float3 samplePos = nearPos; + for(int32_t i=0;i(samplePos, input, c_dimensions.input); + //const T sample = Sampling::sample3D(samplePos, input, c_dimensions.input); + acc = BLEND::blend(acc, sample); + samplePos += step; + } + } + vectorIO::writeVectorType3D(acc, make_int3(globalIdx.x, globalIdx.y, 0), c_dimensions.output, output); + } + //*/ +} + +/* + +__global__ void +__launch_bounds__(BLOCK_SIZE) +kRaymarchGridTransform(){ + T acc = make_cudaFloat(0.f); + for(int32_t i=0;i(i)))); + const T sample = Sampling::sample3D(samplePos, input, c_dimensions.input); + acc = BLEND::blend(sample, acc); + } + vectorIO::writeVector3D(acc, globalIdx.xy, output); +} + +__global__ void +__launch_bounds__(BLOCK_SIZE) +kRaymarchGridTransformGrad(){ + T acc = readVector(globalIdx.xy, output); + T grad = readVector(globalIdx.xy, outputGrad); + for(int32_t i=(z_max-1);i>=0;--i){ + float3 samplePos = getSamplePos(float4(globalIdx.x, globalIdx.y, i, 1.f));; + const T sample = sample(input, samplePos); + acc = blend(sample, acc); + scatterGradInterpolated(grad, samplePos, inputGrad); + } +} +*/ + +template +__host__ inline void LauchRaymarchGridTransformLindepth_SwitchBlend(const Blending::BlendMode blendMode, \ + const dim3 grid, const dim3 block, const GPUDevice& d, \ + const T* input, T* output){ + switch(blendMode){ + case Blending::BLEND_BEERLAMBERT: + LOG("BEERLAMBERT blending mode"); + kRaymarchGridTransformLindepth, FM, BM><<>>(input, output); + //kRaymarchGridTransformLindepth<<>>(input, output); + break; + case Blending::BLEND_ALPHA: + LOG("ALPHA blending mode"); + kRaymarchGridTransformLindepth, FM, BM><<>>(input, output); + break; + case Blending::BLEND_ADDITIVE: + LOG("ADDITIVE blending mode"); + kRaymarchGridTransformLindepth, FM, BM><<>>(input, output); + break; + case Blending::BLEND_ALPHAADDITIVE: + LOG("ALPHAADDITIVE blending mode"); + kRaymarchGridTransformLindepth, FM, BM><<>>(input, output); + break; + default: + LOG("Unknown blending mode"); + throw std::runtime_error("Unknown blending mode"); + } +} +template +__host__ inline void LauchRaymarchGridTransformLindepth_SwitchBoundaryBlend(const Sampling::BoundaryMode boundaryMode, \ + const Blending::BlendMode blendMode, const dim3 grid, const dim3 block, const GPUDevice& d, \ + const T* input, T* output){ + switch(boundaryMode){ + case Sampling::BOUNDARY_BORDER: + LOG("BORDER boundary mode"); + LauchRaymarchGridTransformLindepth_SwitchBlend(blendMode, grid, block, d, input, output); + break; + case Sampling::BOUNDARY_CLAMP: + LOG("CLAMP boundary mode"); + LauchRaymarchGridTransformLindepth_SwitchBlend(blendMode, grid, block, d, input, output); + break; + case Sampling::BOUNDARY_WRAP: + LOG("WRAP boundary mode"); + LauchRaymarchGridTransformLindepth_SwitchBlend(blendMode, grid, block, d, input, output); + break; + default: + LOG("Unknown boundary mode"); + throw std::runtime_error("Unknown boundary mode"); + } +} +template +__host__ inline void LauchRaymarchGridTransformLindepth_SwitchFilterBoundaryBlend(const Sampling::FilterMode filterMode, const Sampling::BoundaryMode boundaryMode, \ + const Blending::BlendMode blendMode, const dim3 grid, const dim3 block, const GPUDevice& d, \ + const T* input, T* output){ + switch(filterMode){ + case Sampling::FILTERMODE_LINEAR: + LOG("LINEAR filter mode"); + LauchRaymarchGridTransformLindepth_SwitchBoundaryBlend(boundaryMode, blendMode, grid, block, d, input, output); + break; + case Sampling::FILTERMODE_NEAREST: + LOG("NEAREST filter mode"); + LauchRaymarchGridTransformLindepth_SwitchBoundaryBlend(boundaryMode, blendMode, grid, block, d, input, output); + break; + case Sampling::FILTERMODE_MIN: + LOG("MIN filter mode"); + LauchRaymarchGridTransformLindepth_SwitchBoundaryBlend(boundaryMode, blendMode, grid, block, d, input, output); + break; + case Sampling::FILTERMODE_MAX: + LOG("MAX filter mode"); + LauchRaymarchGridTransformLindepth_SwitchBoundaryBlend(boundaryMode, blendMode, grid, block, d, input, output); + break; + default: + LOG("Unknown filter mode"); + throw std::runtime_error("Unknown filter mode"); + } + +} +//*/ +template +void RaymarchGridKernelLauncher(const GPUDevice& d, + const T* input, const long long int* input_shape, + const float* M, const float* V, const float* P, const float* _frustum, int32_t numCameras, + const Sampling::FilterMode filterMode, const Sampling::BoundaryMode boundaryMode, + const Blending::BlendMode blendMode, const bool globalSampling, + T* output, const long long int* output_shape){ + + LOG("Start RaymarchGridKernelLauncher"); +#ifdef PROFILING + CUDA_CHECK_RETURN(cudaDeviceSynchronize()); +#endif + + //precompute globals + BEGIN_SAMPLE; + LOG("Set dimensions"); + const size_t batchSize = input_shape[0]; + Dimensions dims; + setDimensions(dims, input_shape, output_shape+1); + LOG("Dimensions in: " << LOG_V3_XYZ(dims.input) << ", pitch: " << dims.input.x*sizeof(T)); + LOG("Dimensions out: " << LOG_V3_XYZ(dims.output) << ", pitch: " << dims.output.x*sizeof(T)); + const int3 kernelDims = make_int3(dims.output.x, dims.output.y, 1); + LOG("Dimensions set"); + + const size_t inputSliceSizeElements = vmath::prod(dims.input); + const size_t outputSliceSizeElements = vmath::prod(kernelDims); + + //Camera params are set per camera + Transformations transforms; + FrustumParams frustum; + int32_t lastCamera=-1; + + END_SAMPLE("Precompute and copy global constants"); + + const dim3 grid = gridDims3D(kernelDims); + const dim3 block = blockDims3D(); + LOG("Sample " << batchSize << " grids with " << numCameras << " cameras"); + LOG("Dimensions grid: " << LOG_V3_XYZ(grid)); + LOG("Dimensions block: " << LOG_V3_XYZ(block)); + + for(size_t batch=0; batch, Sampling::FILTERMODE_LINEAR, Sampling::BOUNDARY_BORDER><<>>(currInput, currOutput); //Blending::BlendState + //kRaymarchGridTransformLindepth<<>>(currInput, currOutput); //Blending::BlendState + LauchRaymarchGridTransformLindepth_SwitchFilterBoundaryBlend(filterMode, boundaryMode, blendMode, grid, block, d, currInput, currOutput); +#ifdef PROFILING + CUDA_CHECK_RETURN(cudaDeviceSynchronize()); +#endif + } + END_SAMPLE("Sample kernel"); + } + } + CUDA_CHECK_RETURN(cudaDeviceSynchronize()); + LOG("End RaymarchGridKernelLauncher"); +} +#define DEFINE_GPU_SPECS(T, C, VEC) \ + template<> \ + void RaymarchGridKernel::operator()(const GPUDevice& d, \ + const T* input, const long long int* input_shape, \ + const float* M, const float* V, const float* P, const float* frustum, int32_t numCameras, \ + const Sampling::FilterMode filterMode, const Sampling::BoundaryMode boundaryMode, \ + const Blending::BlendMode blendMode, const bool globalSampling, \ + T* output, const long long int* output_shape){ \ + RaymarchGridKernelLauncher(d, \ + reinterpret_cast(input), input_shape, \ + M, V, P, frustum, numCameras, \ + filterMode, boundaryMode, blendMode, globalSampling, \ + reinterpret_cast(output), output_shape); \ + } \ + template struct RaymarchGridKernel; +DEFINE_GPU_SPECS(float, 1, float1); +DEFINE_GPU_SPECS(float, 2, float2); +DEFINE_GPU_SPECS(float, 4, float4); + +#undef DEFINE_GPU_SPECS + +/* --- Gradient Pass --- */ + + +template +__global__ void +__launch_bounds__(BLOCK_SIZE) +kNormalize3DGradients(const T* gradIn, sampleCount_t* num_samples, T* gradOut){ + //N.B. gradIn and gradOut may be the same (so don't make them __restrict__) + const int3 globalIdx = globalThreadIdx3D(); + if(isInDimensions(globalIdx, c_dimensions.input)){ + const size_t flatIdx = vectorIO::flatIdx3D(globalIdx.x, globalIdx.y, globalIdx.z, c_dimensions.input); + const sampleCount_t n = num_samples[flatIdx]; + if(n > sampleCount_t(0)){ + const float weight = 1.0f / static_cast(n); + T data = vectorIO::readVectorType(flatIdx, gradIn) * weight; + if(AddGrad){ + data = data + vectorIO::readVectorType(flatIdx, gradOut); + } + vectorIO::writeVectorType(data, flatIdx, gradOut); + + if(SetZero){ + num_samples[flatIdx] = sampleCount_t(0); + } + } + } +} + +template +__global__ void +__launch_bounds__(BLOCK_SIZE) +kRaymarchGridTransformLindepthGrad(const T* input, T* inputGrad, sampleCount_t* sampleCounter, const T* output, const T* outputGrad){ + //back to front + const int3 globalIdx = globalThreadIdx3D(); +#ifdef LOGGING + if(globalIdx.x==0&& globalIdx.y==0&& globalIdx.z==0){ + printf("--- Gradient Kernel running! ---\n"); + printf("K: zMax = %f\n", static_cast(max(c_dimensions.output.z-1, 1))); + } +#endif + if(globalIdx.x(max(c_dimensions.output.z-1, 1)); + const float3 nearPos = make_float3(IDXtoOS(indexToCoords(make_float3(globalIdx.x, globalIdx.y, 0.f)), c_dimensions.output_inv)); + const float3 farPos = make_float3(IDXtoOS(indexToCoords(make_float3(globalIdx.x, globalIdx.y, zMax)), c_dimensions.output_inv)); + const float3 step = (farPos - nearPos) / zMax; + */ + float3 nearPos; + float3 farPos; + float3 step; + int32_t steps; + const bool hit = getRaymarchStep(globalIdx, nearPos, farPos, step, steps); + + if(hit){ + float3 samplePos = farPos; + + T acc = vectorIO::readVectorType3D(globalIdx.x, globalIdx.y, 0, c_dimensions.output, output); + T gradOut = vectorIO::readVectorType3D(globalIdx.x, globalIdx.y, 0, c_dimensions.output, outputGrad); + for(int32_t i=(steps-1);i>=0;--i){ + const T sample = Sampling::sample3D(samplePos, input, c_dimensions.input); + const T grad = BLEND::blendGradients(gradOut, sample, acc); + //TODO this is for linear only, implement other filter modes + Sampling::scatter3D(grad, samplePos, inputGrad, c_dimensions.input, sampleCounter); + samplePos -= step; + } + } + } +} + +template +__host__ inline void LauchRaymarchGridTransformLindepthGrad_SwitchBlend(const Blending::BlendMode blendMode, \ + const dim3 grid, const dim3 block, const GPUDevice& d, \ + const T* input, T* inputGrad, sampleCount_t* sampleCounter, const T* output, const T* outputGrad){ + switch(blendMode){ + case Blending::BLEND_BEERLAMBERT: + LOG("BEERLAMBERT blending mode"); + kRaymarchGridTransformLindepthGrad, FM, BM><<>>(input, inputGrad, sampleCounter, output, outputGrad); + //kRaymarchGridTransformLindepth<<>>(input, output); + break; + case Blending::BLEND_ALPHA: + LOG("ALPHA blending mode"); + kRaymarchGridTransformLindepthGrad, FM, BM><<>>(input, inputGrad, sampleCounter, output, outputGrad); + break; + case Blending::BLEND_ADDITIVE: + LOG("ADDITIVE blending mode"); + kRaymarchGridTransformLindepthGrad, FM, BM><<>>(input, inputGrad, sampleCounter, output, outputGrad); + break; + case Blending::BLEND_ALPHAADDITIVE: + LOG("ALPHAADDITIVE blending mode"); + kRaymarchGridTransformLindepthGrad, FM, BM><<>>(input, inputGrad, sampleCounter, output, outputGrad); + break; + default: + LOG("Unknown blending mode"); + throw std::runtime_error("Unknown blending mode"); + } +} +template +__host__ inline void LauchRaymarchGridTransformLindepthGrad_SwitchBoundaryBlend(const Sampling::BoundaryMode boundaryMode, \ + const Blending::BlendMode blendMode, const dim3 grid, const dim3 block, const GPUDevice& d, \ + const T* input, T* inputGrad, sampleCount_t* sampleCounter, const T* output, const T* outputGrad){ + switch(boundaryMode){ + case Sampling::BOUNDARY_BORDER: + LOG("BORDER boundary mode"); + LauchRaymarchGridTransformLindepthGrad_SwitchBlend(blendMode, grid, block, d, input, inputGrad, sampleCounter, output, outputGrad); + break; + case Sampling::BOUNDARY_CLAMP: + LOG("CLAMP boundary mode"); + LauchRaymarchGridTransformLindepthGrad_SwitchBlend(blendMode, grid, block, d, input, inputGrad, sampleCounter, output, outputGrad); + break; + case Sampling::BOUNDARY_WRAP: + LOG("WRAP boundary mode"); + LauchRaymarchGridTransformLindepthGrad_SwitchBlend(blendMode, grid, block, d, input, inputGrad, sampleCounter, output, outputGrad); + break; + default: + LOG("Unknown boundary mode"); + throw std::runtime_error("Unknown boundary mode"); + } +} +template +__host__ inline void LauchRaymarchGridTransformLindepthGrad_SwitchFilterBoundaryBlend(const Sampling::FilterMode filterMode, const Sampling::BoundaryMode boundaryMode, \ + const Blending::BlendMode blendMode, const dim3 grid, const dim3 block, const GPUDevice& d, \ + const T* input, T* inputGrad, sampleCount_t* sampleCounter, const T* output, const T* outputGrad){ + switch(filterMode){ + case Sampling::FILTERMODE_LINEAR: + LOG("LINEAR filter mode"); + LauchRaymarchGridTransformLindepthGrad_SwitchBoundaryBlend(boundaryMode, blendMode, grid, block, d, input, inputGrad, sampleCounter, output, outputGrad); + break; + case Sampling::FILTERMODE_NEAREST: + LOG("NEAREST filter mode"); + LauchRaymarchGridTransformLindepthGrad_SwitchBoundaryBlend(boundaryMode, blendMode, grid, block, d, input, inputGrad, sampleCounter, output, outputGrad); + break; + case Sampling::FILTERMODE_MIN: + LOG("MIN filter mode"); + LauchRaymarchGridTransformLindepthGrad_SwitchBoundaryBlend(boundaryMode, blendMode, grid, block, d, input, inputGrad, sampleCounter, output, outputGrad); + break; + case Sampling::FILTERMODE_MAX: + LOG("MAX filter mode"); + LauchRaymarchGridTransformLindepthGrad_SwitchBoundaryBlend(boundaryMode, blendMode, grid, block, d, input, inputGrad, sampleCounter, output, outputGrad); + break; + default: + LOG("Unknown filter mode"); + throw std::runtime_error("Unknown filter mode"); + } + +} + + +template +void RaymarchGridKernelLauncherGrad(const GPUDevice& d, + const T* input, T* inputGrad, T* sampleBuffer, sampleCount_t* sampleCounter, const long long int* input_shape, + const float* M, const float* V, const float* P, const float* _frustum, int32_t numCameras, + const Sampling::FilterMode filterMode, const Sampling::BoundaryMode boundaryMode, + const Blending::BlendMode blendMode, const bool globalSampling, + const T* output, const T* outputGrad, const long long int* output_shape){ + + LOG("Start RaymarchGridKernelLauncherGrad"); +#ifdef PROFILING + CUDA_CHECK_RETURN(cudaDeviceSynchronize()); +#endif + + //precompute globals + BEGIN_SAMPLE; + LOG("Set dimensions"); + const size_t batchSize = input_shape[0]; + Dimensions dims; + setDimensions(dims, input_shape, output_shape+1); + LOG("Dimensions in: " << LOG_V3_XYZ(dims.input) << ", pitch: " << dims.input.x*sizeof(T)); + LOG("Dimensions out: " << LOG_V3_XYZ(dims.output) << ", pitch: " << dims.output.x*sizeof(T)); + const int3 kernelDims = make_int3(dims.output.x, dims.output.y, 1); + LOG("Dimensions set"); + + const size_t inputSliceSizeElements = vmath::prod(dims.input); + const size_t outputSliceSizeElements = vmath::prod(kernelDims); + + //Camera params are set per camera + Transformations transforms; + FrustumParams frustum; + int32_t lastCamera=-1; + + END_SAMPLE("Precompute and copy global constants"); + + const dim3 grid = gridDims3D(kernelDims); + const dim3 block = blockDims3D(); + LOG("Sample " << batchSize << " grids with " << numCameras << " cameras"); + LOG("Dimensions grid: " << LOG_V3_XYZ(grid)); + LOG("Dimensions block: " << LOG_V3_XYZ(block)); + + // zero gradient buffers + BEGIN_SAMPLE; + { + checkCudaErrors(cudaMemset(inputGrad, 0, inputSliceSizeElements*sizeof(T)*batchSize)); + if(sampleCounter!=nullptr) checkCudaErrors(cudaMemset(sampleCounter, 0, inputSliceSizeElements*sizeof(sampleCount_t))); + if(sampleBuffer!=nullptr) checkCudaErrors(cudaMemset(sampleBuffer, 0, inputSliceSizeElements*sizeof(T))); +#ifdef PROFILING + CUDA_CHECK_RETURN(cudaDeviceSynchronize()); +#endif + } + END_SAMPLE("Set gradient buffers zero"); + + + for(size_t batch=0; batch0); + T* inputGradBuffer; + if(bufferGradientsForNormalization){ + inputGradBuffer = sampleBuffer; + }else{ + inputGradBuffer = currInputGrad; + } + + BEGIN_SAMPLE; + { + //only set new camera if there are multiple + setTransformations(transforms, M + batch*16, V + camera*16, P + camera*16); + if(lastCamera!=camera){ + setFrustumParams(frustum, _frustum + camera*6); + lastCamera=camera; + } + + } + END_SAMPLE("Set transformation CBuffer"); + + BEGIN_SAMPLE; + { + LauchRaymarchGridTransformLindepthGrad_SwitchFilterBoundaryBlend(filterMode, boundaryMode, blendMode, grid, block, d, \ + currInput, inputGradBuffer, sampleCounter, currOutput, currOutputGrad); +#ifdef PROFILING + CUDA_CHECK_RETURN(cudaDeviceSynchronize()); +#endif + } + END_SAMPLE("Sample kernel"); + + if(NORMALIZE_GRADIENTS!=NORMALIZE_GRADIENT_NONE){ + LOG("Normalize gradients"); + BEGIN_SAMPLE; + { + const dim3 grad_grid = gridDims3D(dims.input); + if(bufferGradientsForNormalization){ + kNormalize3DGradients<<>>(inputGradBuffer, sampleCounter, currInputGrad); + checkCudaErrors(cudaMemset(sampleBuffer, 0, inputSliceSizeElements*sizeof(T))); + }else{ + kNormalize3DGradients<<>>(inputGradBuffer, sampleCounter, currInputGrad); + } + //checkCudaErrors(cudaMemset(sampleCounter, 0, inputSliceSizeElements*sizeof(uint32_t))); +#ifdef PROFILING + CUDA_CHECK_RETURN(cudaDeviceSynchronize()); +#endif + } + END_SAMPLE("Grad normalize"); + } + + } + } + CUDA_CHECK_RETURN(cudaDeviceSynchronize()); + LOG("End RaymarchGridKernelLauncherGrad"); +} +#define DEFINE_GPU_SPECS(T, C, VEC) \ + template<> \ + void RaymarchGridGradKernel::operator()( \ + const GPUDevice& d, \ + const T* input, T* inputGrads, T* sampleBuffer, sampleCount_t* sampleCounter, const long long int* input_shape, \ + const float* M, const float* V, const float* P, const float* frustum, int32_t numCameras, \ + const Sampling::FilterMode filterMode, const Sampling::BoundaryMode boundaryMode, \ + const Blending::BlendMode blendMode, const bool globalSampling, \ + const T* output, const T* outputGrads, const long long int* output_shape \ + ){ \ + RaymarchGridKernelLauncherGrad(d, \ + reinterpret_cast(input), reinterpret_cast(inputGrads), reinterpret_cast(sampleBuffer), sampleCounter, input_shape, \ + M, V, P, frustum, numCameras, \ + filterMode, boundaryMode, blendMode, globalSampling, \ + reinterpret_cast(output), reinterpret_cast(outputGrads), output_shape); \ + } \ + template struct RaymarchGridGradKernel; +DEFINE_GPU_SPECS(float, 1, float1); +DEFINE_GPU_SPECS(float, 2, float2); +DEFINE_GPU_SPECS(float, 4, float4); + + +#undef DEFINE_GPU_SPECS + diff --git a/phitest/render/cuda/src/raymarch_grid.hpp b/phitest/render/cuda/src/raymarch_grid.hpp new file mode 100644 index 0000000..a90179d --- /dev/null +++ b/phitest/render/cuda/src/raymarch_grid.hpp @@ -0,0 +1,58 @@ +#pragma once + +#ifndef _INCLUDE_RAYMARCH_GRID +#define _INCLUDE_RAYMARCH_GRID + +//#include "third_party/eigen3/unsupported/Eigen/CXX11/Tensor" +#include "tensorflow/core/framework/tensor_types.h" +//#include "tensorflow/core/platform/types.h" +#include "sampling_settings_v2.hpp" +#include "blending_settings.hpp" + +using GPUDevice = Eigen::GpuDevice; + +template +struct RaymarchGridKernel{ + void operator()(const Device& d, + const T* input,const long long int* input_shape, + const float* M, const float* V, const float* P, const float* frustum, int32_t numCameras, + const Sampling::FilterMode filterMode, const Sampling::BoundaryMode boundaryMode, + const Blending::BlendMode blendMode, const bool globalSampling, + T* output, const long long int* output_shape); +}; + +#define NORM_GRADS +#define NORM_BY_WEIGHT +enum GradSampleNorm : int32_t {NORMALIZE_GRADIENT_NONE=0, NORMALIZE_GRADIENT_BY_COUNT=1, NORMALIZE_GRADIENT_BY_WEIGHT=2}; + +#ifdef NORM_GRADS + #ifdef NORM_BY_WEIGHT + const GradSampleNorm NORMALIZE_GRADIENTS = NORMALIZE_GRADIENT_BY_WEIGHT; + using sampleCount_t = float; + #define TFsampleCount_t DT_FLOAT + #undef NORM_BY_WEIGHT + #else //by count + const GradSampleNorm NORMALIZE_GRADIENTS = NORMALIZE_GRADIENT_BY_COUNT; + using sampleCount_t = uint32_t; + #define TFsampleCount_t DT_UINT32 + #endif +#undef NORM_GRADS +#else + using sampleCount_t = void; + #define TFsampleCount_t DT_UINT8 + const GradSampleNorm NORMALIZE_GRADIENTS = NORMALIZE_GRADIENT_NONE +#endif + + +template +struct RaymarchGridGradKernel{ + void operator()(const Device& d, + const T* input, T* inputGrads, T* sampleBuffer, sampleCount_t* sampleCounter, const long long int* input_shape, + const float* M, const float* V, const float* P, const float* frustum, int32_t numCameras, + const Sampling::FilterMode filterMode, const Sampling::BoundaryMode boundaryMode, + const Blending::BlendMode blendMode, const bool globalSampling, + const T* output, const T* outputGrads, const long long int* output_shape); +}; + + +#endif //_INCLUDE_RAYMARCH_GRID \ No newline at end of file diff --git a/phitest/render/cuda/src/reduce_blend.cc b/phitest/render/cuda/src/reduce_blend.cc new file mode 100644 index 0000000..9d2c9e9 --- /dev/null +++ b/phitest/render/cuda/src/reduce_blend.cc @@ -0,0 +1,261 @@ +#include "tensorflow/core/framework/op.h" +#include "tensorflow/core/framework/op_kernel.h" +//#include "tensorflow/core/framework/array_ops.h" +#include "tensorflow/core/framework/shape_inference.h" + +#include +#include +//#define LOGGING + +#include "blending_settings.hpp" +#include "render_errors.hpp" + +#ifdef LOGGING +#define MYLOG(msg) std::cout << msg << std::endl +#define LOG_PRINTF(msg) printf(msg) +#else +#define MYLOG(msg) +#define LOG_PRINTF(msg) +#endif + +using namespace tensorflow; + + +REGISTER_OP("ReduceGridBlend") + .Input("input: float32") + .Attr("blending_mode: {'BEER_LAMBERT', 'ALPHA', 'ALPHA_ADDITIVE', 'ADDITIVE'} = 'BEER_LAMBERT'") + .Attr("keep_dims: bool = false") + .Output("output: float32") + .SetShapeFn([](::tensorflow::shape_inference::InferenceContext* c) + { + ::tensorflow::shape_inference::ShapeHandle flatDim; + TF_RETURN_IF_ERROR(c->Subshape(c->input(0), 1, 4, &flatDim)); + c->set_output(0, flatDim); + return Status::OK(); + }); + +// the gradient op +REGISTER_OP("ReduceGridBlendGrad") + .Input("output_grad: float32") + .Input("output: float32") + .Input("input: float32") +// .Input("z_dim: int32") + .Attr("blending_mode: {'BEER_LAMBERT', 'ALPHA', 'ADDITIVE'} = 'BEER_LAMBERT'") //, 'ALPHA_ADDITIVE' + .Attr("keep_dims: bool = false") + .Output("input_grad: float32") + .SetShapeFn([](::tensorflow::shape_inference::InferenceContext* c) + { + /*::tensorflow::shape_inference::ShapeHandle zDim; + TF_RETURN_IF_ERROR(c->MakeShapeFromShapeTensor(1, &zDim)); + TF_RETURN_IF_ERROR(c->Subshape(zDim, 0, 1, &zDim)); + ::tensorflow::shape_inference::ShapeHandle outShape; + TF_RETURN_IF_ERROR(c->Merge(zDim, c->input(0), &outShape)); + */ + c->set_output(0, c->input(2)); + return Status::OK(); + }); + +void GridBlendRKernelLauncher(const float* _input, const long long int* input_shape, + const Blending::BlendMode blend_mode, const bool keep_dims, + float* _output, const long long int* output_shape); +void GridBlendRGKernelLauncher(const float* _input, const long long int* input_shape, + const Blending::BlendMode blend_mode, const bool keep_dims, + float* _output, const long long int* output_shape); +void GridBlendRGBAKernelLauncher(const float* _input, const long long int* input_shape, + const Blending::BlendMode blend_mode, const bool keep_dims, + float* _output, const long long int* output_shape); + +class ReduceGridBlendOp : public OpKernel{ +public: + explicit ReduceGridBlendOp(OpKernelConstruction *context) : OpKernel(context){ + std::string blendingMode; + OP_REQUIRES_OK(context, context->GetAttr("blending_mode", &blendingMode)); + if(blendingMode.compare("BEER_LAMBERT")==0) m_blendMode=Blending::BLEND_BEERLAMBERT; + else if(blendingMode.compare("ALPHA")==0) m_blendMode=Blending::BLEND_ALPHA; + else if(blendingMode.compare("ALPHA_ADDITIVE")==0) m_blendMode=Blending::BLEND_ALPHAADDITIVE; + else if(blendingMode.compare("ADDITIVE")==0) m_blendMode=Blending::BLEND_ADDITIVE; + else {OP_REQUIRES(context, false, errors::InvalidArgument("invalid blending mode"));} + + OP_REQUIRES_OK(context, context->GetAttr("keep_dims", &m_keepDims)); + } + + void Compute(OpKernelContext *context) override{ + + MYLOG("reduce blend op kernel start"); + + const Tensor& input_grid = context->input(0); + + MYLOG("Check input"); + TensorShape input_shape = input_grid.shape(); + OP_REQUIRES(context, (input_grid.dims()==5 && input_shape.dim_size(4)<=4), + errors::InvalidArgument("Invalid input shape", input_shape.DebugString())); + const int64 batch = input_shape.dim_size(0); + const int64 channel = input_shape.dim_size(4); + + MYLOG("Create output shape"); + TensorShape output_shape; + output_shape.AddDim(batch); + if(m_keepDims) output_shape.AddDim(input_shape.dim_size(1)); + output_shape.AddDim(input_shape.dim_size(2)); + output_shape.AddDim(input_shape.dim_size(3)); + output_shape.AddDim(channel); + + + MYLOG("Allocate output"); + Tensor* output_grid = NULL; + OP_REQUIRES_OK(context, context->allocate_output(0, output_shape, &output_grid)); + MYLOG("Check allocated output"); + // auto out = output_grid->flat(); + // MYLOG("Allocated output size: " << out.size() << " - " << output_grid->NumElements()); + if(!m_keepDims) output_shape.InsertDim(1,1); //fill z dim, NDHWC needed for correct kernel dimensions handling + MYLOG("output_shape: " << output_shape.dim_size(0) << ", " << output_shape.dim_size(1) << ", " << output_shape.dim_size(2) << ", " << output_shape.dim_size(3)); + + MYLOG("Reduce"); + try{ + switch(channel){ + case 1: + GridBlendRKernelLauncher(input_grid.flat().data(), input_shape.dim_sizes().data(), + m_blendMode, m_keepDims, + output_grid->flat().data(), output_shape.dim_sizes().data() + ); + break; + case 2: + GridBlendRGKernelLauncher(input_grid.flat().data(), input_shape.dim_sizes().data(), + m_blendMode, m_keepDims, + output_grid->flat().data(), output_shape.dim_sizes().data() + ); + break; + case 4: + GridBlendRGBAKernelLauncher(input_grid.flat().data(), input_shape.dim_sizes().data(), + m_blendMode, m_keepDims, + output_grid->flat().data(), output_shape.dim_sizes().data() + ); + break; + default: + OP_REQUIRES(context, false, + errors::Unimplemented("Only 1,2 and 4 Channel supported.")); + } + } catch(RenderError::RenderError& e){ + OP_REQUIRES(context, false, errors::Internal(e.what())); + } + MYLOG("Kernel done"); + } +private: + Blending::BlendMode m_blendMode; + bool m_keepDims; + +}; + + +void ReduceGridBlendRGradKernelLauncher(const float* _input, float* _input_grads, const long long int* input_shape, + const Blending::BlendMode blend_mode, const bool keep_dims, + const float* _output, const float* _output_grads, const long long int* output_shape); +void ReduceGridBlendRGGradKernelLauncher(const float* _input, float* _input_grads, const long long int* input_shape, + const Blending::BlendMode blend_mode, const bool keep_dims, + const float* _output, const float* _output_grads, const long long int* output_shape); +void ReduceGridBlendRGBAGradKernelLauncher(const float* _input, float* _input_grads, const long long int* input_shape, + const Blending::BlendMode blend_mode, const bool keep_dims, + const float* _output, const float* _output_grads, const long long int* output_shape); + +class ReduceGridBlendGradOp : public OpKernel{ +public: + explicit ReduceGridBlendGradOp(OpKernelConstruction *context) : OpKernel(context){ + std::string blendingMode; + OP_REQUIRES_OK(context, context->GetAttr("blending_mode", &blendingMode)); + if(blendingMode.compare("BEER_LAMBERT")==0) m_blendMode=Blending::BLEND_BEERLAMBERT; + else if(blendingMode.compare("ALPHA")==0) m_blendMode=Blending::BLEND_ALPHA; + else if(blendingMode.compare("ALPHA_ADDITIVE")==0) m_blendMode=Blending::BLEND_ALPHAADDITIVE; + else if(blendingMode.compare("ADDITIVE")==0) m_blendMode=Blending::BLEND_ADDITIVE; + else {OP_REQUIRES(context, false, errors::InvalidArgument("invalid blending mode"));} + + OP_REQUIRES_OK(context, context->GetAttr("keep_dims", &m_keepDims)); + //OP_REQUIRES_OK(context, context->GetAttr("inverse_transform", &m_inverseTransform)); + } + + void Compute(OpKernelContext *context) override{ + + MYLOG("reduce blend gradient op kernel start"); + + const Tensor& output_grads = context->input(0); + const Tensor& output_grid = context->input(1); + const Tensor& input_grid = context->input(2); + + MYLOG("Check input"); + TensorShape input_shape = input_grid.shape(); + OP_REQUIRES(context, (input_grid.dims()==5 && input_shape.dim_size(4)<=4), + errors::InvalidArgument("Invalid input shape", input_shape.DebugString())); + const int64 batch = input_shape.dim_size(0); + const int64 channel = input_shape.dim_size(4); + + MYLOG("Check output (gradients)"); + OP_REQUIRES(context, output_grads.shape() == output_grid.shape(), + errors::InvalidArgument("Mismatch between output gradients and output shape: ", output_grads.shape().DebugString(), output_grid.shape().DebugString())); + TensorShape output_shape = output_grads.shape(); + if(m_keepDims){ + OP_REQUIRES(context, (output_grads.dims()==5 && output_shape.dim_size(4)==channel), + errors::InvalidArgument("Invalid output gradients shape", output_shape.DebugString())); + OP_REQUIRES(context, output_shape==input_shape, + errors::InvalidArgument("output gradients shape does not match input shape", input_shape.DebugString())); + }else{ + OP_REQUIRES(context, (output_grads.dims()==4 && output_shape.dim_size(3)==channel), + errors::InvalidArgument("Invalid output gradients shape", output_shape.DebugString())); + OP_REQUIRES(context, output_shape.dim_size(1)==input_shape.dim_size(2)&& + output_shape.dim_size(2)==input_shape.dim_size(3), + errors::InvalidArgument("output gradients shape does not match input shape (x,y)", input_shape.DebugString())); + output_shape.InsertDim(1,1); //fill z dim + } + MYLOG("output_shape: " << output_shape.dim_size(0) << ", " << output_shape.dim_size(1) << ", " << output_shape.dim_size(2) << ", " << output_shape.dim_size(3)); + + + MYLOG("Allocate input gradients"); + Tensor* input_grads = NULL; + OP_REQUIRES_OK(context, context->allocate_output(0, input_shape, &input_grads)); + MYLOG("Check allocated input gradients"); + auto out = input_grads->flat(); + MYLOG("Allocated input gradients size: " << out.size() << " - " << input_grads->NumElements()); + + MYLOG("Reduce"); + try{ + switch(channel){ + case 1: + ReduceGridBlendRGradKernelLauncher(input_grid.flat().data(), input_grads->flat().data(), input_shape.dim_sizes().data(), + m_blendMode, m_keepDims, + output_grid.flat().data(), output_grads.flat().data(), output_shape.dim_sizes().data() + ); + break; + case 2: + ReduceGridBlendRGGradKernelLauncher(input_grid.flat().data(), input_grads->flat().data(), input_shape.dim_sizes().data(), + m_blendMode, m_keepDims, + output_grid.flat().data(), output_grads.flat().data(), output_shape.dim_sizes().data() + ); + break; + case 4: + ReduceGridBlendRGBAGradKernelLauncher(input_grid.flat().data(), input_grads->flat().data(), input_shape.dim_sizes().data(), + m_blendMode, m_keepDims, + output_grid.flat().data(), output_grads.flat().data(), output_shape.dim_sizes().data() + ); + break; + default: + OP_REQUIRES(context, false, + errors::Unimplemented("Only 1,2 and 4 Channel supported.")); + } + }catch(RenderError::RenderError& e){ + OP_REQUIRES(context, false, errors::Internal(e.what())); + } + + MYLOG("Kernel done"); + } +private: + Blending::BlendMode m_blendMode; + bool m_keepDims; + +}; + +REGISTER_KERNEL_BUILDER(Name("ReduceGridBlend") \ + .Device(DEVICE_GPU) \ + , ReduceGridBlendOp); + +REGISTER_KERNEL_BUILDER(Name("ReduceGridBlendGrad") \ + .Device(DEVICE_GPU) \ + , ReduceGridBlendGradOp); + diff --git a/phitest/render/cuda/src/reduce_blend.cu.cc b/phitest/render/cuda/src/reduce_blend.cu.cc new file mode 100644 index 0000000..0b96cff --- /dev/null +++ b/phitest/render/cuda/src/reduce_blend.cu.cc @@ -0,0 +1,245 @@ + +#include +#include "cuda-samples/Common/helper_cuda.h" +#include +#include + +#include "glm/vec3.hpp" +#include "glm/vec4.hpp" + +#include "vectormath_helper.hpp" + +#define GLM_ENABLE_EXPERIMENTAL +#include + +#include "vector_io.hpp" + +//defines for kernel setup +#define BLOCK_SIZE_X 32 +#define BLOCK_SIZE_Y 8 + +#define CBUF_DIMENSIONS +//#define CBUF_DIMENSIONS_INVERSE + +//#define LOGGING +//#define PROFILING + +#include "kernel_setup.hpp" +#include "render_errors.hpp" + +#include "blending.hpp" + + + +template +//2D groups moving along z of 3D input and blending to 2D output, writing the output after each step +__global__ void kBlend(const T* input, T* output){ + MAKE_GLOBAL_INDEX; + if(isInDimensions(globalIdx, c_dimensions.output) && globalIdx.z==0){ + glm::ivec3 position = globalIdx; + const int32_t depth = c_dimensions.input.z; + + T acc = {0}; + for(int32_t i=0;i(position, c_dimensions.input, input); + acc = BLEND::blend(acc, data); //blend(acc, data); + if(KeepDims) vectorIO::writeVectorType3D(acc, position, c_dimensions.output, output); + } + if(!KeepDims) vectorIO::writeVectorType3D(acc, globalIdx, c_dimensions.output, output); + } +} + + +template +//2D groups moving along z of 3D input and blending to 2D output, writing the output after each step +__global__ void kBlendGrad(const T* input, T* input_grads, const T* output, const T* output_grads){ + MAKE_GLOBAL_INDEX; + if(isInDimensions(globalIdx, c_dimensions.output) && globalIdx.z==0){ + glm::ivec3 position = globalIdx; + const int32_t depth = c_dimensions.input.z; + + T grads = {0}; + T prev_out = {0}; + if(!KeepDims){ + grads = vectorIO::readVectorType3D(position, c_dimensions.output, output_grads); + prev_out = vectorIO::readVectorType3D(position, c_dimensions.output, output); + } + for(int32_t i=depth-1;i>=0;--i){ + position.z = i; + if(KeepDims){ + grads += vectorIO::readVectorType3D(position, c_dimensions.output, output_grads); + prev_out = vectorIO::readVectorType3D(position, c_dimensions.output, output); + } + const T data = vectorIO::readVectorType3D(position, c_dimensions.input, input); + const T d_cellOut = BLEND::blendGradients(grads, data, prev_out); //blendGrad(grads, data, prev_out); + vectorIO::writeVectorType3D(d_cellOut, position, c_dimensions.input, input_grads); + } + } +} + +template +void ReduceGridBlendKernelLauncher(const float* _input, const long long int* input_shape, + const Blending::BlendMode blend_mode, const bool keep_dims, + float* _output, const long long int* output_shape){ + const T* input = reinterpret_cast(_input); + T* output = reinterpret_cast(_output); + + LOG("Set dimensions"); + const size_t batchSize = input_shape[0]; + Dimensions dims; + setDimensions(dims, input_shape, output_shape); + + const size_t inputSliceSizeElements = vmath::prod(dims.input); + const size_t outputSliceSizeElements = vmath::prod(dims.output); + + glm::ivec3 compute_dims = dims.output; + compute_dims.z = 1; + const dim3 grid(GRID_DIMS(compute_dims)); + const dim3 block(BLOCK_DIMS); + LOG("Sample " << batchSize << " grids"); + for(size_t batch=0; batch, true><<>>(input, output); + }else{ + kBlend, false><<>>(input, output); + } + break; + case Blending::BLEND_ALPHA: + if(keep_dims){ + kBlend, true><<>>(input, output); + }else{ + kBlend, false><<>>(input, output); + } + break; + case Blending::BLEND_ALPHAADDITIVE: + if(keep_dims){ + kBlend, true><<>>(input, output); + }else{ + kBlend, false><<>>(input, output); + } + break; + case Blending::BLEND_ADDITIVE: + if(keep_dims){ + kBlend, true><<>>(input, output); + }else{ + kBlend, false><<>>(input, output); + } + break; + default: + throw RenderError::RenderError("Unkown blend_mode"); + } + //CUDA_CHECK_RETURN(cudaDeviceSynchronize()); + } + END_SAMPLE("blend kernel"); + input += inputSliceSizeElements; + output += outputSliceSizeElements; + } + cudaError_t err = cudaDeviceSynchronize(); + if(err!=cudaSuccess){ + throw RenderError::CudaError(RenderError::Formatter() << + __FILE__ << "[" << __LINE__ << "]: Cuda error '" << cudaGetErrorString(err) << "' (" << err << ") in ReduceGridBlendKernelLauncher(). " << + "input shape " << LOG_V3_XYZ(dims.input) << ", output shape " << LOG_V3_XYZ(dims.output) + ); + } +} + +void GridBlendRKernelLauncher(const float* _input, const long long int* input_shape, + const Blending::BlendMode blend_mode, const bool keep_dims, + float* _output, const long long int* output_shape){ + ReduceGridBlendKernelLauncher(_input, input_shape, blend_mode, keep_dims, _output, output_shape); +} +void GridBlendRGKernelLauncher(const float* _input, const long long int* input_shape, + const Blending::BlendMode blend_mode, const bool keep_dims, + float* _output, const long long int* output_shape){ + ReduceGridBlendKernelLauncher(_input, input_shape, blend_mode, keep_dims, _output, output_shape); +} +void GridBlendRGBAKernelLauncher(const float* _input, const long long int* input_shape, + const Blending::BlendMode blend_mode, const bool keep_dims, + float* _output, const long long int* output_shape){ + ReduceGridBlendKernelLauncher(_input, input_shape, blend_mode, keep_dims, _output, output_shape); +} + +template +void ReduceGridBlendGradKernelLauncher(const float* _input, float* _input_grads, const long long int* input_shape, + const Blending::BlendMode blend_mode, const bool keep_dims, + const float* _output, const float* _output_grads, const long long int* output_shape){ + const T* input = reinterpret_cast(_input); + T* input_grads = reinterpret_cast(_input_grads); + const T* output = reinterpret_cast(_output); + const T* output_grads = reinterpret_cast(_output_grads); + + LOG("Set dimensions"); + const size_t batchSize = input_shape[0]; + Dimensions dims; + setDimensions(dims, input_shape, output_shape); + + const size_t inputSliceSizeElements = vmath::prod(dims.input); + const size_t outputSliceSizeElements = vmath::prod(dims.output); + + glm::ivec3 compute_dims = dims.output; + compute_dims.z = 1; + const dim3 grid(GRID_DIMS(compute_dims)); + const dim3 block(BLOCK_DIMS); + LOG("Sample " << batchSize << " grids"); + for(size_t batch=0; batch, true><<>>(input, input_grads, output, output_grads);} \ + else{kBlendGrad, false><<>>(input, input_grads, output, output_grads);} \ +break; + case Blending::BLEND_BEERLAMBERT: + if(keep_dims){ + kBlendGrad, true><<>>(input, input_grads, output, output_grads); + }else{ + kBlendGrad, false><<>>(input, input_grads, output, output_grads); + } + break; + BLEND_CASE(BLEND_ALPHA) + BLEND_CASE(BLEND_ADDITIVE) +#undef BLEND_CASE + default: + throw RenderError::RenderError("Unkown blend_mode"); + } + //CUDA_CHECK_RETURN(cudaDeviceSynchronize()); + } + END_SAMPLE("blend grads kernel"); + input += inputSliceSizeElements; + input_grads += inputSliceSizeElements; + output += outputSliceSizeElements; + output_grads += outputSliceSizeElements; + } + cudaError_t err = cudaDeviceSynchronize(); + if(err!=cudaSuccess){ + throw RenderError::CudaError(RenderError::Formatter() << + __FILE__ << "[" << __LINE__ << "]: Cuda error '" << cudaGetErrorString(err) << "' (" << err << ") in ReduceGridBlendGradKernelLauncher(). " << + "input shape " << LOG_V3_XYZ(dims.input) << ", output shape " << LOG_V3_XYZ(dims.output) + ); + } +} + +void ReduceGridBlendRGradKernelLauncher(const float* _input, float* _input_grads, const long long int* input_shape, + const Blending::BlendMode blend_mode, const bool keep_dims, + const float* _output, const float* _output_grads, const long long int* output_shape){ + ReduceGridBlendGradKernelLauncher(_input, _input_grads, input_shape, blend_mode, keep_dims, _output, _output_grads, output_shape); +} +void ReduceGridBlendRGGradKernelLauncher(const float* _input, float* _input_grads, const long long int* input_shape, + const Blending::BlendMode blend_mode, const bool keep_dims, + const float* _output, const float* _output_grads, const long long int* output_shape){ + ReduceGridBlendGradKernelLauncher(_input, _input_grads, input_shape, blend_mode, keep_dims, _output, _output_grads, output_shape); +} +void ReduceGridBlendRGBAGradKernelLauncher(const float* _input, float* _input_grads, const long long int* input_shape, + const Blending::BlendMode blend_mode, const bool keep_dims, + const float* _output, const float* _output_grads, const long long int* output_shape){ + ReduceGridBlendGradKernelLauncher(_input, _input_grads, input_shape, blend_mode, keep_dims, _output, _output_grads, output_shape); +} + diff --git a/phitest/render/cuda/src/render_errors.hpp b/phitest/render/cuda/src/render_errors.hpp new file mode 100644 index 0000000..23f9039 --- /dev/null +++ b/phitest/render/cuda/src/render_errors.hpp @@ -0,0 +1,61 @@ +#pragma once + +#ifndef _INCLUDE_RENDER_ERRORS +#define _INCLUDE_RENDER_ERRORS + +//#include +#include +#include +//#include + +namespace RenderError{ + +class RenderInternalError : public std::logic_error{ +public: + RenderInternalError(const std::string& msg): std::logic_error(msg){} +}; + +class RenderError : public std::runtime_error{ +public: + RenderError(const std::string& msg): std::runtime_error(msg){} +}; + + +class CudaError : public RenderError{ +public: + CudaError(const std::string& msg): RenderError(msg){} +}; + +//https://stackoverflow.com/questions/12261915/how-to-throw-stdexceptions-with-variable-messages +class Formatter +{ +public: + Formatter() {} + ~Formatter() {} + + template + Formatter & operator << (const Type & value) + { + stream_ << value; + return *this; + } + + std::string str() const { return stream_.str(); } + operator std::string () const { return stream_.str(); } + + enum ConvertToString + { + to_str + }; + std::string operator >> (ConvertToString) { return stream_.str(); } + +private: + std::stringstream stream_; + + Formatter(const Formatter &); + Formatter & operator = (Formatter &); +}; + +} + +#endif //_INCLUDE_RENDER_ERRORS \ No newline at end of file diff --git a/phitest/render/cuda/src/resample_grid.cc b/phitest/render/cuda/src/resample_grid.cc new file mode 100644 index 0000000..f680f99 --- /dev/null +++ b/phitest/render/cuda/src/resample_grid.cc @@ -0,0 +1,1221 @@ +#include "tensorflow/core/framework/op.h" +#include "tensorflow/core/framework/op_kernel.h" +//#include "tensorflow/core/framework/array_ops.h" +#include "tensorflow/core/framework/shape_inference.h" + +#include +#include +//#define LOGGING +#include "resample_grid.hpp" +#include "render_errors.hpp" + +#ifdef LOGGING +#define MYLOG(msg) std::cout << __FILE__ << "[" << __LINE__ << "]: " << msg << std::endl +#define LOG_PRINTF(msg) printf(msg) +#else +#define MYLOG(msg) +#define LOG_PRINTF(msg) +#endif + +using namespace tensorflow; + +// Sample at transformed 3D grid positions from an input grid +REGISTER_OP("SampleGridTransform") + .Attr("T: {float}") + .Input("input: T") // NDHWC + .Input("matrix_m: float32") // 4x4 matrix or batch N thereof + .Input("matrix_v: float32") // 4x4 matrix or batch V thereof + .Input("matrix_p: float32") // 4x4 matrix or batch V thereof + .Input("frustum_params: float32") // 6 elemnts or batch V thereof +// .Input("lookup: float32") //LuT if used + .Input("output_shape: int32") // DHW + //.Attr("sampling_mode: {'LINEAR', 'LINEAR_MIP_LINEAR', 'TEX_LINEAR', 'NEAREST', 'TEX_NEAREST'} = 'LINEAR_MIP_LINEAR'") + .Attr("interpolation: {'NEAREST', 'LINEAR', 'QUADRATIC'} = 'LINEAR'") + .Attr("boundary: {'BORDER', 'CLAMP'} = 'BORDER'") + .Attr("mipmapping: {'NONE', 'NEAREST', 'LINEAR'} = 'NONE'") + .Attr("num_mipmaps: int = 0") + .Attr("mip_bias: float = 0.0") + //.Attr("inverse_transform: bool = false") +// .Attr("use_texture: bool = false") // use texture memory for sampling, not compatible with all modes + .Attr("coordinate_mode: {'TRANSFORM_LINDEPTH', 'TRANSFORM_LINDEPTH_REVERSE', 'TRANSFORM', 'TRANSFORM_REVERSE'} = 'TRANSFORM_LINDEPTH'") //, 'RAY' + .Attr("cell_center_offset: float = 0.5") // offset of cell center coordinates from cell indices. should be 0.5 for all transform modes and if mipmapping is used + .Attr("separate_camera_batch: bool = true") + .Output("output: T") // NVDHWC + .SetShapeFn([](::tensorflow::shape_inference::InferenceContext* c) + { + ::tensorflow::shape_inference::ShapeHandle inShape = c->input(0); + ::tensorflow::shape_inference::ShapeHandle outShape; + TF_RETURN_IF_ERROR(c->MakeShapeFromShapeTensor(3, &outShape)); + TF_RETURN_IF_ERROR(c->Subshape(outShape, 0, 3, &outShape)); + + //prepend camera batch + bool globalSampling; + TF_RETURN_IF_ERROR(c->GetAttr("separate_camera_batch", &globalSampling)); + if(globalSampling){ + TF_RETURN_IF_ERROR(c->Concatenate(c->Vector(c->UnknownDim()), outShape, &outShape)); + }else{ + TF_RETURN_IF_ERROR(c->Concatenate(c->Vector(1), outShape, &outShape)); + } + //prepend data batch + TF_RETURN_IF_ERROR(c->Concatenate(c->Vector(c->Dim(inShape, 0)), outShape, &outShape)); + //append channel + TF_RETURN_IF_ERROR(c->Concatenate(outShape, c->Vector(c->Dim(inShape, -1)), &outShape)); + c->set_output(0, outShape); + return Status::OK(); + }); + +// the gradient op +REGISTER_OP("SampleGridTransformGrad") + .Input("input: float32") // NDHWC + .Input("output_grad: float32") // NVDHWC + .Input("matrix_m: float32") // 4x4 matrix or batch N thereof + .Input("matrix_v: float32") // 4x4 matrix or batch V thereof + .Input("matrix_p: float32") // 4x4 matrix or batch V thereof + .Input("frustum_params: float32") +// .Input("lookup: float32") //LuT if used + //.Input("input_shape: int32") // DHW + .Attr("interpolation: {'NEAREST', 'LINEAR', 'QUADRATIC'} = 'LINEAR'") + .Attr("boundary: {'BORDER', 'CLAMP'} = 'BORDER'") + .Attr("mipmapping: {'NONE'} = 'NONE'") //, 'NEAREST', 'LINEAR'. currently not supported + .Attr("num_mipmaps: int = 0") + .Attr("mip_bias: float = 0.0") + .Attr("coordinate_mode: {'TRANSFORM_LINDEPTH', 'TRANSFORM_LINDEPTH_REVERSE', 'TRANSFORM', 'TRANSFORM_REVERSE'} = 'TRANSFORM_LINDEPTH'") //, 'RAY' + .Attr("cell_center_offset: float = 0.5") // offset of cell center coordinates from cell indices. should be 0.5 for all transform modes and if mipmapping is used + .Attr("separate_camera_batch: bool = true") + .Output("input_grad: float32") +// .Output("lookup_grad: float32") // None if lookup is not used + .SetShapeFn([](::tensorflow::shape_inference::InferenceContext* c) + { + c->set_output(0, c->input(0)); +// c->set_output(1, c->input(6)); // TODO: if used + return Status::OK(); + }); + +REGISTER_OP("SampleGridLut") + .Input("input: float32") // NDHWC + .Input("lookup: float32") //Lookup Table, defines output shape + //.Attr("sampling_mode: {'LINEAR', 'LINEAR_MIP_LINEAR', 'TEX_LINEAR', 'NEAREST', 'TEX_NEAREST'} = 'LINEAR_MIP_LINEAR'") + .Attr("interpolation: {'NEAREST', 'LINEAR', 'MIN', 'MAX'} = 'LINEAR'") + .Attr("boundary: {'BORDER', 'CLAMP', 'WRAP'} = 'BORDER'") + .Attr("mipmapping: {'NONE', 'NEAREST', 'LINEAR'} = 'NONE'") + .Attr("num_mipmaps: int = 0") + .Attr("mip_bias: float = 0.0") + //.Attr("inverse_transform: bool = false") + //.Attr("use_texture: bool = false") // use texture memory for sampling, not compatible with all modes + .Attr("coordinate_mode: {'LOOKUP'} = 'LOOKUP'") //, 'RAY' + .Attr("cell_center_offset: float = 0.0") // offset of cell center coordinates from cell indices. should be 0.5 for all transform modes and if mipmapping is used + .Attr("separate_camera_batch: bool = true") + .Attr("relative_coords: bool = true") + .Attr("normalized_coords: bool = false") + .Output("output: float32") // NVDHWC + .SetShapeFn([](::tensorflow::shape_inference::InferenceContext* c) + { + ::tensorflow::shape_inference::ShapeHandle channel; + TF_RETURN_IF_ERROR(c->Subshape(c->input(0), 3, &channel)); + ::tensorflow::shape_inference::ShapeHandle outShape; + TF_RETURN_IF_ERROR(c->Subshape(c->input(1), 0, 3, &outShape)); + TF_RETURN_IF_ERROR(c->Concatenate(outShape, channel, &outShape)); + c->set_output(0, outShape); + return Status::OK(); + }); + + +// the gradient op +REGISTER_OP("SampleGridLutGrad") + .Input("input: float32") // NDHWC + .Input("output_grad: float32") // NVDHWC +// .Input("matrix_m: float32") // 4x4 matrix or batch N thereof +// .Input("matrix_v: float32") // 4x4 matrix or batch V thereof +// .Input("matrix_p: float32") // 4x4 matrix or batch V thereof +// .Input("frustum_params: float32") + .Input("lookup: float32") //LuT if used + //.Input("input_shape: int32") // DHW + .Attr("interpolation: {'NEAREST', 'LINEAR', 'MIN', 'MAX'} = 'LINEAR'") + .Attr("boundary: {'BORDER', 'CLAMP', 'WRAP'} = 'BORDER'") + .Attr("mipmapping: {'NONE'} = 'NONE'") //, 'NEAREST', 'LINEAR'. currently not supported + .Attr("num_mipmaps: int = 0") + .Attr("mip_bias: float = 0.0") + .Attr("coordinate_mode: {'LOOKUP'} = 'LOOKUP'") //, 'RAY' + .Attr("cell_center_offset: float = 0.0") // offset of cell center coordinates from cell indices. should be 0.5 for all transform modes and if mipmapping is used + .Attr("separate_camera_batch: bool = true") + .Attr("relative_coords: bool = true") + .Attr("normalized_coords: bool = false") + .Output("input_grad: float32") + .Output("lookup_grad: float32") // None if lookup is not used + .SetShapeFn([](::tensorflow::shape_inference::InferenceContext* c) + { + c->set_output(0, c->input(0)); + c->set_output(1, c->input(2)); + return Status::OK(); + }); + +REGISTER_OP("LodTransform") + .Input("input_shape: int32") //DHW shape of the grid that is sampled FROM + .Input("matrix_mv: float32") + .Input("matrix_p: float32") + .Input("frustum_params: float32") + .Input("output_shape: int32") //DHW shape of the grid that is sampled TO + .Attr("inverse_transform: bool = false") // if false: sample from object grid to frustum grid, else: vice versa + .Attr("linearize_depth: bool = true") // + .Attr("relative_coords: bool = false") + .Output("output: float32") //VDHWC V-batch of sampling coordinates, C=4 (x,y,z,LoD) + .SetShapeFn([](::tensorflow::shape_inference::InferenceContext* c) + { + ::tensorflow::shape_inference::ShapeHandle channel; + TF_RETURN_IF_ERROR(c->Subshape(c->input(0), 3, &channel)); + ::tensorflow::shape_inference::ShapeHandle outShape; + TF_RETURN_IF_ERROR(c->MakeShapeFromShapeTensor(3, &outShape)); + TF_RETURN_IF_ERROR(c->Subshape(outShape, 0, 3, &outShape)); + TF_RETURN_IF_ERROR(c->Merge(outShape, channel, &outShape)); + c->set_output(0, outShape); + return Status::OK(); + }); +/* +enum SamplingMode{ //fast|linear + Nearest=0, // 0|0 + Linear=1, // 0|1 + NearestFast=2, // 1|0 + LinearFast=3, // 1|1 +};*/ + +Status makeShapeFromTensor(const Tensor& sizes, TensorShape* shape){ + MYLOG("Create shape from tensor"); + auto sizes_flat = sizes.flat(); + const int64 num_dims = sizes_flat.size(); + for(int64 i=0; iAddDim(dim); + } + return Status::OK(); +} + +bool isValidTransformMatrix(const TensorShape& matrixShape, int32_t& numMatrices){ + if(matrixShape.dims()==2){ + numMatrices = 1; + return matrixShape.dim_size(0)==matrixShape.dim_size(1) && matrixShape.dim_size(0)==4; + } else if(matrixShape.dims()==3){ + numMatrices = matrixShape.dim_size(0); + return matrixShape.dim_size(1)==matrixShape.dim_size(2) && matrixShape.dim_size(1)==4; + } else{ + numMatrices = 0; + return false; + } +} + +bool isValidFrustumParams(const TensorShape& shape, int32_t& numCameras){ + if(shape.dims()==1){ + numCameras = 1; + return shape.dim_size(0)==6; + }else if(shape.dims()==2){ + numCameras = shape.dim_size(0); + return shape.dim_size(1)==6; + }else{ + numCameras = 0; + return false; + } +} + +/* + calculate linear memory needed for mips and atlas + set numMips to maximum possible level of mips with the given dimensions +*/ +size_t setupMipAtlas(const TensorShape& base_shape, Sampling::SamplerSettings& samplingSettings, const size_t elementSizeBytes, const size_t prtSizeBytes, const size_t alignment=128){ + + #define ALIGN_ADDR_UP(addr) (((addr) + (alignment-1) ) & ~(alignment-1)) + size_t mipsSizeBytes = ALIGN_ADDR_UP((samplingSettings.mipLevel+1) * prtSizeBytes) + alignment; + + for(int32_t m=1; m<(samplingSettings.mipLevel+1); ++m){ + size_t elements = (base_shape.dim_size(1)>>m) * (base_shape.dim_size(2)>>m) * (base_shape.dim_size(3)>>m); + if(elements<=0){ //too small, mip would be empty + samplingSettings.mipLevel = m-1; + break; + } + mipsSizeBytes += ALIGN_ADDR_UP(elements * elementSizeBytes); + } + //no mips, so turn off the mipmapping system + if(samplingSettings.mipLevel==0){ + samplingSettings.mipMode==Sampling::SamplerSettings::MIPMODE_NONE; + } + + return mipsSizeBytes; +} + +#define COND_SET_SM(SM) (samplingMode.compare(#SM)==0) mode=Sampling::SM +bool setSamplingMode(Sampling::SamplingMode &mode, const std::string samplingMode){ + if COND_SET_SM(LINEAR); + else if COND_SET_SM(LINEAR_MIP_LINEAR); + else if COND_SET_SM(TEX_LINEAR); + else if COND_SET_SM(NEAREST); + else if COND_SET_SM(TEX_NEAREST); + else return false; + return true; +} +#undef COND_SET_SM + +#define COND_SET_CM(s,CM) (coordinateMode.compare(#s)==0) mode=Sampling::CM +bool setCoordinateMode(Sampling::CoordinateMode &mode, const std::string coordinateMode){ + if COND_SET_CM(TRANSFORM_LINDEPTH,TransformLinDepth); + else if COND_SET_CM(TRANSFORM_LINDEPTH_REVERSE,TransformLinDepthReverse); + else if COND_SET_CM(TRANSFORM,Transform); + else if COND_SET_CM(TRANSFORM_REVERSE,TransformReverse); + else if COND_SET_CM(LOOKUP,LuT); + else return false; + return true; +} +#undef COND_SET_CM + +/* +void SampleRGridKernelLauncher( + const void* input, const long long int* input_shape, + const float* M, const float* V, const float* P, const float* frustum, int32_t numCameras, + uint8_t* mipAtlas, + const Sampling::CoordinateMode coordinateMode, + const Sampling::SamplerSettings, const bool globalSampling, + void* output, const long long int* output_shape); +void SampleRGGridKernelLauncher( + const void* input, const long long int* input_shape, + const float* M, const float* V, const float* P, const float* frustum, int32_t numCameras, + uint8_t* mipAtlas, + const Sampling::CoordinateMode coordinateMode, + const Sampling::SamplerSettings, const bool globalSampling, + void* output, const long long int* output_shape); +void SampleRGBAGridKernelLauncher( + const void* input, const long long int* input_shape, + const float* M, const float* V, const float* P, const float* frustum, int32_t numCameras, + uint8_t* mipAtlas, + const Sampling::CoordinateMode coordinateMode, + const Sampling::SamplerSettings, const bool globalSampling, + void* output, const long long int* output_shape); +*/ + +#if GOOGLE_CUDA +#define DECLARE_GPU_SPEC(T, C) \ + template<> \ + void SampleGridKernel::operator()( \ + const GPUDevice& d, \ + const void* input, const long long int* input_shape, \ + const float* M, const float* V, const float* P, const float* frustum, int32_t numCameras, \ + uint8_t* mipAtlas, \ + const Sampling::CoordinateMode coordinateMode, \ + const Sampling::SamplerSettings, const bool globalSampling, \ + void* output, const long long int* output_shape \ + ); \ + extern template struct SampleGridKernel; +DECLARE_GPU_SPEC(float, 1) +DECLARE_GPU_SPEC(float, 2) +DECLARE_GPU_SPEC(float, 4) +#undef DECLARE_GPU_SPEC +#endif + +//only for 3D grids with up to 4 channel +//no support for batches yet? +template +class SampleGridTransformOp : public OpKernel{ +public: + explicit SampleGridTransformOp(OpKernelConstruction *context) : OpKernel(context){ + //std::string samplingMode; + //OP_REQUIRES_OK(context, context->GetAttr("sampling_mode", &samplingMode)); + //OP_REQUIRES(context, setSamplingMode(m_samplingMode, samplingMode), errors::InvalidArgument("invalid sampling mode")); + + memset(&m_samplingSettings, 0, sizeof(Sampling::SamplerSettings)); + std::string s_interpolation; + OP_REQUIRES_OK(context, context->GetAttr("interpolation", &s_interpolation)); + if(s_interpolation.compare("QUADRATIC")==0) m_samplingSettings.filterMode = Sampling::SamplerSettings::FILTERMODE_QUADRATIC; + else if(s_interpolation.compare("LINEAR")==0) m_samplingSettings.filterMode = Sampling::SamplerSettings::FILTERMODE_LINEAR; + else if(s_interpolation.compare("MIN")==0) m_samplingSettings.filterMode = Sampling::SamplerSettings::FILTERMODE_MIN; + else if(s_interpolation.compare("MAX")==0) m_samplingSettings.filterMode = Sampling::SamplerSettings::FILTERMODE_MAX; + + std::string s_boundary; + OP_REQUIRES_OK(context, context->GetAttr("boundary", &s_boundary)); + if(s_boundary.compare("CLAMP")==0) m_samplingSettings.boundaryMode = Sampling::SamplerSettings::CLAMP; + if(s_boundary.compare("WRAP")==0) m_samplingSettings.boundaryMode = Sampling::SamplerSettings::WRAP; + if(s_boundary.compare("MIRROR")==0) m_samplingSettings.boundaryMode = Sampling::SamplerSettings::MIRROR; + + std::string s_mipmapping; + OP_REQUIRES_OK(context, context->GetAttr("mipmapping", &s_mipmapping)); + if(s_mipmapping.compare("NEAREST")==0) m_samplingSettings.mipMode = Sampling::SamplerSettings::MIPMODE_NEAREST; + else if(s_mipmapping.compare("LINEAR")==0) m_samplingSettings.mipMode = Sampling::SamplerSettings::MIPMODE_LINEAR; + + OP_REQUIRES_OK(context, context->GetAttr("num_mipmaps", &m_samplingSettings.mipLevel)); + OP_REQUIRES(context, m_samplingSettings.mipLevel>0 || m_samplingSettings.mipMode==Sampling::SamplerSettings::MIPMODE_NONE, + errors::InvalidArgument("when using mipmaps num_mipmaps must be larger than 0.")); + + OP_REQUIRES_OK(context, context->GetAttr("mip_bias", &m_samplingSettings.mipBias)); + + //OP_REQUIRES_OK(context, context->GetAttr("use_texture", &m_samplingSettings.useTexture)); + + //OP_REQUIRES_OK(context, context->GetAttr("inverse_transform", &m_inverseTransform)); + OP_REQUIRES_OK(context, context->GetAttr("separate_camera_batch", &m_globalSampling)); + //OP_REQUIRES_OK(context, context->GetAttr("use_mipmaps", &m_mipmaps)); + std::string s_coordmode; + OP_REQUIRES_OK(context, context->GetAttr("coordinate_mode", &s_coordmode)); + OP_REQUIRES(context, setCoordinateMode(m_coordinateMode, s_coordmode), + errors::InvalidArgument("invalid coordinate_mode.")); + OP_REQUIRES_OK(context, context->GetAttr("cell_center_offset", &m_samplingSettings.cellCenterOffset)); + if(m_coordinateMode!=Sampling::LuT){ + OP_REQUIRES(context, m_samplingSettings.cellCenterOffset==0.5f, + errors::InvalidArgument("Invalid cell center offset for given coordinate mode.")); + } + } + + void Compute(OpKernelContext *context) override{ + + MYLOG("Sample transform op kernel start"); + + const Tensor& input_grid = context->input(0); + const Tensor& tensor_M = context->input(1); + const Tensor& tensor_V = context->input(2); + const Tensor& tensor_P = context->input(3); + const Tensor& frustum = context->input(4); + //const Tensor& tensor_lookup = context->input(5); + const Tensor& output_shape_tensor = context->input(5); + + //check input + MYLOG("Check input"); + TensorShape input_shape = input_grid.shape(); + OP_REQUIRES(context, input_grid.dims()==5 && input_shape.dim_size(4)<=4, + errors::InvalidArgument("Invalid input shape (NDHWC):", input_shape.DebugString())); + OP_REQUIRES(context, 0<(input_shape.dim_size(0)*input_shape.dim_size(1)*input_shape.dim_size(2)*input_shape.dim_size(3)*input_shape.dim_size(4)), + errors::InvalidArgument("Empty input (NDHWC):", input_shape.DebugString())); + const int64 batch = input_shape.dim_size(0); + const int64 channel = input_shape.dim_size(4); + OP_REQUIRES(context, !(channel==3||channel>4), + errors::Unimplemented("Only 1,2 and 4 Channel supported.")); + + //check output shape + MYLOG("Check output shape tensor"); + OP_REQUIRES(context, output_shape_tensor.dims()==1 && output_shape_tensor.dim_size(0)==3, errors::InvalidArgument("Invalid output_shape")); + MYLOG("Create output shape"); + TensorShape output_shape; + OP_REQUIRES_OK(context, makeShapeFromTensor(output_shape_tensor, &output_shape)); + MYLOG("Check output shape"); + OP_REQUIRES(context, output_shape.dims()==3, + errors::InvalidArgument("Invalid output_shape")); + OP_REQUIRES(context, 0<(output_shape.dim_size(0)*output_shape.dim_size(1)*output_shape.dim_size(2)), + errors::InvalidArgument("Empty output (NDHWC):", output_shape.DebugString())); + output_shape.InsertDim(0,batch); + output_shape.AddDim(channel); + MYLOG("output_shape: " << output_shape.dim_size(0) << ", " << output_shape.dim_size(1) << ", " << output_shape.dim_size(2) << ", " << output_shape.dim_size(3)); + + //check transform matrics + int32_t numCameras=0; + //const float* raw_lookup = nullptr; + OP_REQUIRES(context, m_coordinateMode==Sampling::TransformLinDepth || m_coordinateMode==Sampling::TransformLinDepthReverse + || m_coordinateMode==Sampling::Transform || m_coordinateMode==Sampling::TransformReverse, + errors::InvalidArgument("Invalid coordinate_mode")); + //if(m_coordinateMode==Sampling::TransformLinDepth || m_coordinateMode==Sampling::TransformLinDepthReverse + // || m_coordinateMode==Sampling::Transform || m_coordinateMode==Sampling::TransformReverse){ + MYLOG("Check transform"); + int32 numMatrix_M=0, numMatrix_V=0, numMatrix_P=0, numFrustum=0; + OP_REQUIRES(context, isValidTransformMatrix(tensor_M.shape(), numMatrix_M), + errors::InvalidArgument("transformation matrix_m must be a 4x4 square matrix or a batch of 4x4 square matrices:", tensor_M.shape().DebugString())); + OP_REQUIRES(context, numMatrix_M==batch, errors::InvalidArgument("model matrix batch size mismatch")); + + OP_REQUIRES(context, isValidTransformMatrix(tensor_V.shape(), numMatrix_V), + errors::InvalidArgument("transformation matrix_v must be a 4x4 square matrix or a batch of 4x4 square matrices:", tensor_V.shape().DebugString())); + OP_REQUIRES(context, isValidTransformMatrix(tensor_P.shape(), numMatrix_P), + errors::InvalidArgument("transformation matrix_p must be a 4x4 square matrix or a batch of 4x4 square matrices:", tensor_P.shape().DebugString())); + OP_REQUIRES(context, isValidFrustumParams(frustum.shape(), numFrustum), + errors::InvalidArgument("frustum must be a 1D tensor of 6 elements or a batch thereof:", frustum.shape().DebugString())); + //TODO + OP_REQUIRES(context, numMatrix_V==numMatrix_P && numMatrix_V==numFrustum, errors::InvalidArgument("camera batch size mismatch")); + numCameras = numMatrix_V; + /*}else if(m_coordinateMode==Sampling::LuT){ + MYLOG("Check LuT"); + TensorShape lut_shape = tensor_lookup.shape(); + OP_REQUIRES(context, lut_shape.dims()==5 && lut_shape.dim_size(4)==4 + && lut_shape.dim_size(1)==output_shape.dim_size(1) + && lut_shape.dim_size(2)==output_shape.dim_size(2) + && lut_shape.dim_size(3)==output_shape.dim_size(3), + errors::InvalidArgument("Invalid lut shape (NDHWC):", lut_shape.DebugString(), " with C=4, DHW must macht output shape:", output_shape.DebugString())); + numCameras = lut_shape.dim_size(0); + }*/ + if(!m_globalSampling){ + OP_REQUIRES(context, numCameras==batch, errors::InvalidArgument("camera batch must match data batch when not using global sampling.")); + output_shape.InsertDim(1,1); + }else{ + output_shape.InsertDim(1,numCameras); + } + + + + //allocate outout + MYLOG("Allocate output"); + //OP_REQUIRES(context, output_shape.IsValid(), errors::InvalidArgument("Broken output shape")); + Tensor* output_grid = NULL; + OP_REQUIRES_OK(context, context->allocate_output(0, output_shape, &output_grid)); + MYLOG("Check allocated output\n"); + auto out = output_grid->flat(); + MYLOG("Allocated output size: " << out.size() << " - " << output_grid->NumElements()); + + //allocate temporary mip atlas + uint8_t* mipAtlas = nullptr; + Tensor mip_atlas; + size_t mipAtlasSize = setupMipAtlas(input_shape, m_samplingSettings, sizeof(float)*channel, sizeof(float*)); + if(m_samplingSettings.mipMode!=Sampling::SamplerSettings::MIPMODE_NONE){ + MYLOG("Allocate mips"); + TensorShape mip_atlas_shape; + mip_atlas_shape.AddDim(mipAtlasSize); + OP_REQUIRES_OK(context, context->allocate_temp(DT_UINT8, mip_atlas_shape, &mip_atlas)); + mipAtlas = mip_atlas.flat().data(); + } + + + //TODO handle arbitrary amount of channel + // - move channel dimension outwards (to batch) and handle only 1-channel case internally. would also handle batches + // this would benefit from NCHW layout, otherwise have to transpose + // or just require NCHW as input format (or NHWC with up to 4 channel, the rest has to be packed in N) and let tensorflow/user handle the conversion... + // - split into up-to-4-channel partitions. might be faster? but is harder to handle + + MYLOG("Resample"); + switch(channel){ + case 1: + SampleGridKernel()(context->eigen_device(), + input_grid.flat().data(), input_shape.dim_sizes().data(), + tensor_M.flat().data(), tensor_V.flat().data(), tensor_P.flat().data(), frustum.flat().data(), numCameras, + mipAtlas, + m_coordinateMode, + m_samplingSettings, m_globalSampling, + output_grid->flat().data(), output_shape.dim_sizes().data()); + break; + case 2: + SampleGridKernel()(context->eigen_device(), + input_grid.flat().data(), input_shape.dim_sizes().data(), + tensor_M.flat().data(), tensor_V.flat().data(), tensor_P.flat().data(), frustum.flat().data(), numCameras, + mipAtlas, + m_coordinateMode, + m_samplingSettings, m_globalSampling, + output_grid->flat().data(), output_shape.dim_sizes().data()); + break; + case 4: + SampleGridKernel()(context->eigen_device(), + input_grid.flat().data(), input_shape.dim_sizes().data(), + tensor_M.flat().data(), tensor_V.flat().data(), tensor_P.flat().data(), frustum.flat().data(), numCameras, + mipAtlas, + m_coordinateMode, + m_samplingSettings, m_globalSampling, + output_grid->flat().data(), output_shape.dim_sizes().data()); + break; + default: + OP_REQUIRES(context, false, + errors::Unimplemented("Only 1,2 and 4 Channel supported.")); + } + + MYLOG("Kernel done"); + } +private: + bool m_inverseTransform; + bool m_globalSampling; + //Sampling::SamplingMode m_samplingMode; + Sampling::SamplerSettings m_samplingSettings; + Sampling::CoordinateMode m_coordinateMode; +}; + + +void SampleRGridGradKernelLauncher(const void* _input, const long long int* input_shape, + const float* M, const float* V, const float* P, const float* _frustum, int32_t numCameras, + const Sampling::CoordinateMode coordinateMode, + const Sampling::SamplerSettings samplingSettings, const bool globalSampling, + const void* _output_grad, const long long int* output_shape, + void* _input_grad, uint32_t* sample_count_buffer); +void SampleRGGridGradKernelLauncher(const void* _input, const long long int* input_shape, + const float* M, const float* V, const float* P, const float* _frustum, int32_t numCameras, + const Sampling::CoordinateMode coordinateMode, + const Sampling::SamplerSettings samplingSettings, const bool globalSampling, + const void* _output_grad, const long long int* output_shape, + void* _input_grad, uint32_t* sample_count_buffer); +void SampleRGBAGridGradKernelLauncher(const void* _input, const long long int* input_shape, + const float* M, const float* V, const float* P, const float* _frustum, int32_t numCameras, + const Sampling::CoordinateMode coordinateMode, + const Sampling::SamplerSettings samplingSettings, const bool globalSampling, + const void* _output_grad, const long long int* output_shape, + void* _input_grad, uint32_t* sample_count_buffer); + +class SampleGridTransformGradOp : public OpKernel{ +public: + explicit SampleGridTransformGradOp(OpKernelConstruction *context) : OpKernel(context){ + + memset(&m_samplingSettings, 0, sizeof(Sampling::SamplerSettings)); + std::string s_interpolation; + OP_REQUIRES_OK(context, context->GetAttr("interpolation", &s_interpolation)); + if(s_interpolation.compare("QUADRATIC")==0) m_samplingSettings.filterMode = Sampling::SamplerSettings::FILTERMODE_QUADRATIC; + else if(s_interpolation.compare("LINEAR")==0) m_samplingSettings.filterMode = Sampling::SamplerSettings::FILTERMODE_LINEAR; + else if(s_interpolation.compare("MIN")==0) m_samplingSettings.filterMode = Sampling::SamplerSettings::FILTERMODE_MIN; + else if(s_interpolation.compare("MAX")==0) m_samplingSettings.filterMode = Sampling::SamplerSettings::FILTERMODE_MAX; + std::string s_boundary; + OP_REQUIRES_OK(context, context->GetAttr("boundary", &s_boundary)); + if(s_boundary.compare("CLAMP")==0) m_samplingSettings.boundaryMode = Sampling::SamplerSettings::CLAMP; + if(s_boundary.compare("WRAP")==0) m_samplingSettings.boundaryMode = Sampling::SamplerSettings::WRAP; + if(s_boundary.compare("MIRROR")==0) m_samplingSettings.boundaryMode = Sampling::SamplerSettings::MIRROR; + + std::string s_mipmapping; + OP_REQUIRES_OK(context, context->GetAttr("mipmapping", &s_mipmapping)); + if(s_mipmapping.compare("NEAREST")==0) m_samplingSettings.mipMode = Sampling::SamplerSettings::MIPMODE_NEAREST; + else if(s_mipmapping.compare("LINEAR")==0) m_samplingSettings.mipMode = Sampling::SamplerSettings::MIPMODE_LINEAR; + + OP_REQUIRES_OK(context, context->GetAttr("num_mipmaps", &m_samplingSettings.mipLevel)); + OP_REQUIRES(context, m_samplingSettings.mipLevel>0 || m_samplingSettings.mipMode==Sampling::SamplerSettings::MIPMODE_NONE, + errors::InvalidArgument("when using mipmaps num_mipmaps must be larger than 0.")); + + OP_REQUIRES_OK(context, context->GetAttr("mip_bias", &m_samplingSettings.mipBias)); + + OP_REQUIRES_OK(context, context->GetAttr("separate_camera_batch", &m_globalSampling)); + std::string s_coordmode; + OP_REQUIRES_OK(context, context->GetAttr("coordinate_mode", &s_coordmode)); + OP_REQUIRES(context, setCoordinateMode(m_coordinateMode, s_coordmode), + errors::InvalidArgument("invalid coordinate_mode.")); + OP_REQUIRES_OK(context, context->GetAttr("cell_center_offset", &m_samplingSettings.cellCenterOffset)); + if(m_coordinateMode!=Sampling::LuT){ + OP_REQUIRES(context, m_samplingSettings.cellCenterOffset==0.5f, + errors::InvalidArgument("Invalid cell center offset for given coordinate mode.")); + } + } + + void Compute(OpKernelContext *context) override{ + + MYLOG("Gradient kernel start"); + + const Tensor& input_grid = context->input(0); + const Tensor& output_grad_grid = context->input(1); + const Tensor& tensor_M = context->input(2); + const Tensor& tensor_V = context->input(3); + const Tensor& tensor_P = context->input(4); + const Tensor& frustum = context->input(5); + const Tensor& tensor_lookup = context->input(6); + + //check input + MYLOG("Check input"); + TensorShape input_shape = input_grid.shape(); + OP_REQUIRES(context, input_grid.dims()==5 && input_shape.dim_size(4)<=4, + errors::InvalidArgument("Invalid input shape (NDHWC):", input_shape.DebugString())); + const int64 batch = input_shape.dim_size(0); + const int64 channel = input_shape.dim_size(4); + + //check output gradients + MYLOG("Check output_grads"); + TensorShape output_shape = output_grad_grid.shape(); + OP_REQUIRES(context, output_grad_grid.dims()==6 && output_shape.dim_size(5)<=4, + errors::InvalidArgument("Invalid output_grads shape (NVDHWC):", output_shape.DebugString())); + OP_REQUIRES(context, output_shape.dim_size(0)==batch, + errors::InvalidArgument("output_grads batch size does not match input batch size.")); + + + //check transform matrics + int32_t numCameras=0; + OP_REQUIRES(context, m_coordinateMode==Sampling::TransformLinDepth || m_coordinateMode==Sampling::TransformLinDepthReverse + || m_coordinateMode==Sampling::Transform || m_coordinateMode==Sampling::TransformReverse, + errors::InvalidArgument("Invalid coordinate_mode")); + // if(m_coordinateMode==Sampling::TransformLinDepth || m_coordinateMode==Sampling::TransformLinDepthReverse + // || m_coordinateMode==Sampling::Transform || m_coordinateMode==Sampling::TransformReverse){ + MYLOG("Check transform"); + int32 numMatrix_M=0, numMatrix_V=0, numMatrix_P=0, numFrustum=0; + OP_REQUIRES(context, isValidTransformMatrix(tensor_M.shape(), numMatrix_M), + errors::InvalidArgument("transformation matrix_m must be a 4x4 square matrix or a batch of 4x4 square matrices:", tensor_M.shape().DebugString())); + OP_REQUIRES(context, numMatrix_M==batch, errors::InvalidArgument("model matrix batch size mismatch")); + + OP_REQUIRES(context, isValidTransformMatrix(tensor_V.shape(), numMatrix_V), + errors::InvalidArgument("transformation matrix_v must be a 4x4 square matrix or a batch of 4x4 square matrices:", tensor_V.shape().DebugString())); + OP_REQUIRES(context, isValidTransformMatrix(tensor_P.shape(), numMatrix_P), + errors::InvalidArgument("transformation matrix_p must be a 4x4 square matrix or a batch of 4x4 square matrices:", tensor_P.shape().DebugString())); + OP_REQUIRES(context, isValidFrustumParams(frustum.shape(), numFrustum), + errors::InvalidArgument("frustum must be a 1D tensor of 6 elements or a batch thereof:", frustum.shape().DebugString())); + //TODO + OP_REQUIRES(context, numMatrix_V==numMatrix_P && numMatrix_V==numFrustum, errors::InvalidArgument("camera batch size mismatch")); + numCameras = numMatrix_V; + /* }else if(m_coordinateMode==Sampling::LuT){ + MYLOG("Check LuT"); + TensorShape lut_shape = tensor_lookup.shape(); + OP_REQUIRES(context, lut_shape.dims()==5 && lut_shape.dim_size(4)==4 + && lut_shape.dim_size(1)==output_shape.dim_size(2) + && lut_shape.dim_size(2)==output_shape.dim_size(3) + && lut_shape.dim_size(3)==output_shape.dim_size(4), + errors::InvalidArgument("Invalid lut shape (VDHWC):", lut_shape.DebugString(), ", DHW must macht output shape:", output_shape.DebugString())); + numCameras = lut_shape.dim_size(0); + }*/ + if(!m_globalSampling){ + OP_REQUIRES(context, numCameras==batch, errors::InvalidArgument("camera batch must match data batch when not using global sampling.")); + + OP_REQUIRES(context, output_shape.dim_size(1)==1, + errors::InvalidArgument("output_grads views size must be 1 if not using global sampling.")); + }else{ + OP_REQUIRES(context, output_shape.dim_size(1)==numCameras, + errors::InvalidArgument("output_grads views size does not match cameras.")); + } + + uint32_t* sample_count_buffer = nullptr; + Tensor sample_count_tensor; + if(NORMALIZE_GRADIENTS){ + MYLOG("Allocate gradient counter"); + TensorShape sample_count_tensor_shape; + sample_count_tensor_shape.AddDim(input_shape.dim_size(1)); + sample_count_tensor_shape.AddDim(input_shape.dim_size(2)); + sample_count_tensor_shape.AddDim(input_shape.dim_size(3)); + OP_REQUIRES_OK(context, context->allocate_temp(DT_UINT32, sample_count_tensor_shape, &sample_count_tensor)); + sample_count_buffer = sample_count_tensor.flat().data(); + } + + //allocate outout + MYLOG("Allocate input gradients"); + //OP_REQUIRES(context, output_shape.IsValid(), errors::InvalidArgument("Broken output shape")); + Tensor* input_grads = nullptr; + OP_REQUIRES_OK(context, context->allocate_output(0, input_shape, &input_grads)); + + /* Tensor* lookup_grads = nullptr; + float* raw_lookup_grads = nullptr; + if(m_coordinateMode==Sampling::LuT){ + TensorShape lut_shape = tensor_lookup.shape(); + OP_REQUIRES_OK(context, context->allocate_output(1, lut_shape, &lookup_grads)); + raw_lookup_grads = lookup_grads->flat().data(); + }else{ + TensorShape lut_shape; + OP_REQUIRES_OK(context, context->allocate_output(1, lut_shape, &lookup_grads)); + }*/ + + + MYLOG("Resample\n"); + switch(channel){ + case 1: + SampleRGridGradKernelLauncher(input_grid.flat().data(), input_shape.dim_sizes().data(), + tensor_M.flat().data(), tensor_V.flat().data(), tensor_P.flat().data(), frustum.flat().data(), numCameras, + m_coordinateMode, + m_samplingSettings, m_globalSampling, + output_grad_grid.flat().data(), output_shape.dim_sizes().data(), + input_grads->flat().data(), sample_count_buffer); + break; + case 2: + SampleRGGridGradKernelLauncher(input_grid.flat().data(), input_shape.dim_sizes().data(), + tensor_M.flat().data(), tensor_V.flat().data(), tensor_P.flat().data(), frustum.flat().data(), numCameras, + m_coordinateMode, + m_samplingSettings, m_globalSampling, + output_grad_grid.flat().data(), output_shape.dim_sizes().data(), + input_grads->flat().data(), sample_count_buffer); + break; + case 4: + SampleRGBAGridGradKernelLauncher(input_grid.flat().data(), input_shape.dim_sizes().data(), + tensor_M.flat().data(), tensor_V.flat().data(), tensor_P.flat().data(), frustum.flat().data(), numCameras, + m_coordinateMode, + m_samplingSettings, m_globalSampling, + output_grad_grid.flat().data(), output_shape.dim_sizes().data(), + input_grads->flat().data(), sample_count_buffer); + break; + default: + OP_REQUIRES(context, false, + errors::Unimplemented("Only 1,2 and 4 Channel supported.")); + } + //*/ + MYLOG("Gradient kernel done"); + } +private: + bool m_globalSampling; + //Sampling::SamplingMode m_samplingMode; + Sampling::SamplerSettings m_samplingSettings; + Sampling::CoordinateMode m_coordinateMode; +}; + + +void SampleRGridLuTKernelLauncher( + const void* input, const long long int* input_shape, + int32_t numCameras, + const float* lookup, uint8_t* mipAtlas, + const Sampling::CoordinateMode coordinateMode, + Sampling::SamplerSettings, const bool globalSampling, const bool relative, const bool normalized, + void* output, const long long int* output_shape); +void SampleRGGridLuTKernelLauncher( + const void* input, const long long int* input_shape, + int32_t numCameras, + const float* lookup, uint8_t* mipAtlas, + const Sampling::CoordinateMode coordinateMode, + Sampling::SamplerSettings, const bool globalSampling, const bool relative, const bool normalized, + void* output, const long long int* output_shape); +void SampleRGBAGridLuTKernelLauncher( + const void* input, const long long int* input_shape, + int32_t numCameras, + const float* lookup, uint8_t* mipAtlas, + const Sampling::CoordinateMode coordinateMode, + Sampling::SamplerSettings, const bool globalSampling, const bool relative, const bool normalized, + void* output, const long long int* output_shape); + +class SampleGridLuTOp : public OpKernel{ +public: + explicit SampleGridLuTOp(OpKernelConstruction *context) : OpKernel(context){ + //std::string samplingMode; + //OP_REQUIRES_OK(context, context->GetAttr("sampling_mode", &samplingMode)); + //OP_REQUIRES(context, setSamplingMode(m_samplingMode, samplingMode), errors::InvalidArgument("invalid sampling mode")); + + memset(&m_samplingSettings, 0, sizeof(Sampling::SamplerSettings)); + std::string s_interpolation; + OP_REQUIRES_OK(context, context->GetAttr("interpolation", &s_interpolation)); + if(s_interpolation.compare("LINEAR")==0) m_samplingSettings.filterMode = Sampling::SamplerSettings::FILTERMODE_LINEAR; + else if(s_interpolation.compare("MIN")==0) m_samplingSettings.filterMode = Sampling::SamplerSettings::FILTERMODE_MIN; + else if(s_interpolation.compare("MAX")==0) m_samplingSettings.filterMode = Sampling::SamplerSettings::FILTERMODE_MAX; + + std::string s_boundary; + OP_REQUIRES_OK(context, context->GetAttr("boundary", &s_boundary)); + if(s_boundary.compare("CLAMP")==0) m_samplingSettings.boundaryMode = Sampling::SamplerSettings::CLAMP; + if(s_boundary.compare("WRAP")==0) m_samplingSettings.boundaryMode = Sampling::SamplerSettings::WRAP; + if(s_boundary.compare("MIRROR")==0) m_samplingSettings.boundaryMode = Sampling::SamplerSettings::MIRROR; + + std::string s_mipmapping; + OP_REQUIRES_OK(context, context->GetAttr("mipmapping", &s_mipmapping)); + if(s_mipmapping.compare("NEAREST")==0) m_samplingSettings.mipMode = Sampling::SamplerSettings::MIPMODE_NEAREST; + else if(s_mipmapping.compare("LINEAR")==0) m_samplingSettings.mipMode = Sampling::SamplerSettings::MIPMODE_LINEAR; + + OP_REQUIRES_OK(context, context->GetAttr("num_mipmaps", &m_samplingSettings.mipLevel)); + OP_REQUIRES(context, m_samplingSettings.mipLevel>0 || m_samplingSettings.mipMode==Sampling::SamplerSettings::MIPMODE_NONE, + errors::InvalidArgument("when using mipmaps num_mipmaps must be larger than 0.")); + + OP_REQUIRES_OK(context, context->GetAttr("mip_bias", &m_samplingSettings.mipBias)); + + //OP_REQUIRES_OK(context, context->GetAttr("inverse_transform", &m_inverseTransform)); + OP_REQUIRES_OK(context, context->GetAttr("separate_camera_batch", &m_globalSampling)); + OP_REQUIRES_OK(context, context->GetAttr("relative_coords", &m_relativeCoords)); + OP_REQUIRES_OK(context, context->GetAttr("normalized_coords", &m_normalizedCoords)); + //OP_REQUIRES_OK(context, context->GetAttr("use_mipmaps", &m_mipmaps)); + std::string s_coordmode; + OP_REQUIRES_OK(context, context->GetAttr("coordinate_mode", &s_coordmode)); + OP_REQUIRES(context, setCoordinateMode(m_coordinateMode, s_coordmode), + errors::InvalidArgument("invalid coordinate_mode.")); + OP_REQUIRES_OK(context, context->GetAttr("cell_center_offset", &m_samplingSettings.cellCenterOffset)); + if(m_normalizedCoords){ + OP_REQUIRES(context, m_samplingSettings.cellCenterOffset==0.5f, + errors::InvalidArgument("Invalid cell center offset must be 0.5 when using normalized coordinates.")); + } + } + + void Compute(OpKernelContext *context) override{ + + MYLOG("Sample transform op kernel start"); + + const Tensor& input_grid = context->input(0); + const Tensor& tensor_lookup = context->input(1); + + //check input + MYLOG("Check input"); + TensorShape input_shape = input_grid.shape(); + OP_REQUIRES(context, input_grid.dims()==5 && input_shape.dim_size(4)<=4, + errors::InvalidArgument("Invalid input shape (NDHWC):", input_shape.DebugString())); + const int64 batch = input_shape.dim_size(0); + const int64 channel = input_shape.dim_size(4); + + MYLOG("Check LuT"); + OP_REQUIRES(context, m_coordinateMode==Sampling::LuT, + errors::InvalidArgument("Invalid coorindate_mode")); + TensorShape lut_shape = tensor_lookup.shape(); + OP_REQUIRES(context, lut_shape.dims()==5 && lut_shape.dim_size(4)==4, + errors::InvalidArgument("Invalid lut shape (NDHWC) with C=4:", lut_shape.DebugString())); + int32_t numCameras = lut_shape.dim_size(0); + + //check output shape + MYLOG("Create output shape"); + TensorShape output_shape; + output_shape.AddDim(lut_shape.dim_size(1)); + output_shape.AddDim(lut_shape.dim_size(2)); + output_shape.AddDim(lut_shape.dim_size(3)); + output_shape.InsertDim(0,batch); + output_shape.AddDim(channel); + MYLOG("output_shape: " << output_shape.dim_size(0) << ", " << output_shape.dim_size(1) << ", " << output_shape.dim_size(2) << ", " << output_shape.dim_size(3)); + + if(!m_globalSampling){ + OP_REQUIRES(context, numCameras==batch, errors::InvalidArgument("camera batch must match data batch when not using global sampling.")); + output_shape.InsertDim(1,1); + }else{ + output_shape.InsertDim(1,numCameras); + } + + + + //allocate outout + MYLOG("Allocate output"); + //OP_REQUIRES(context, output_shape.IsValid(), errors::InvalidArgument("Broken output shape")); + Tensor* output_grid = NULL; + OP_REQUIRES_OK(context, context->allocate_output(0, output_shape, &output_grid)); + MYLOG("Check allocated output\n"); + auto out = output_grid->flat(); + MYLOG("Allocated output size: " << out.size() << " - " << output_grid->NumElements()); + + //allocate temporary mip atlas + uint8_t* mipAtlas = nullptr; + Tensor mip_atlas; + size_t mipAtlasSize = setupMipAtlas(input_shape, m_samplingSettings, sizeof(float)*channel, sizeof(float*)); + if(m_samplingSettings.mipMode!=Sampling::SamplerSettings::MIPMODE_NONE){ + MYLOG("Allocate mips"); + TensorShape mip_atlas_shape; + mip_atlas_shape.AddDim(mipAtlasSize); + OP_REQUIRES_OK(context, context->allocate_temp(DT_UINT8, mip_atlas_shape, &mip_atlas)); + mipAtlas = mip_atlas.flat().data(); + } + + + //TODO handle arbitrary amount of channel + // - move channel dimension outwards (to batch) and handle only 1-channel case internally. would also handle batches + // this would benefit from NCHW layout, otherwise have to transpose + // or just require NCHW as input format (or NHWC with up to 4 channel, the rest has to be packed in N) and let tensorflow/user handle the conversion... + // - split into up-to-4-channel partitions. might be faster? but is harder to handle + + MYLOG("Resample"); + switch(channel){ + case 1: + SampleRGridLuTKernelLauncher(input_grid.flat().data(), input_shape.dim_sizes().data(), + numCameras, + tensor_lookup.flat().data(), mipAtlas, + m_coordinateMode, + m_samplingSettings, m_globalSampling, m_relativeCoords, m_normalizedCoords, + output_grid->flat().data(), output_shape.dim_sizes().data()); + break; + case 2: + SampleRGGridLuTKernelLauncher(input_grid.flat().data(), input_shape.dim_sizes().data(), + numCameras, + tensor_lookup.flat().data(), mipAtlas, + m_coordinateMode, + m_samplingSettings, m_globalSampling, m_relativeCoords, m_normalizedCoords, + output_grid->flat().data(), output_shape.dim_sizes().data()); + break; + case 4: + SampleRGBAGridLuTKernelLauncher(input_grid.flat().data(), input_shape.dim_sizes().data(), + numCameras, + tensor_lookup.flat().data(), mipAtlas, + m_coordinateMode, + m_samplingSettings, m_globalSampling, m_relativeCoords, m_normalizedCoords, + output_grid->flat().data(), output_shape.dim_sizes().data()); + break; + default: + OP_REQUIRES(context, false, + errors::Unimplemented("Only 1,2 and 4 Channel supported.")); + } + + MYLOG("Kernel done"); + } +private: + bool m_globalSampling; + bool m_relativeCoords; + bool m_normalizedCoords; + //Sampling::SamplingMode m_samplingMode; + Sampling::SamplerSettings m_samplingSettings; + Sampling::CoordinateMode m_coordinateMode; +}; + + + +void SampleRGridLuTGradKernelLauncher(const void* _input, const long long int* input_shape, + int32_t numCameras, + const float* _lookup, + const Sampling::CoordinateMode coordinateMode, + const Sampling::SamplerSettings samplingSettings, const bool globalSampling, const bool relative, const bool normalized, + const void* _output_grad, const long long int* output_shape, + void* _input_grad, void* _lookup_grad, uint32_t* sample_count_buffer); +void SampleRGGridLuTGradKernelLauncher(const void* _input, const long long int* input_shape, + int32_t numCameras, + const float* _lookup, + const Sampling::CoordinateMode coordinateMode, + const Sampling::SamplerSettings samplingSettings, const bool globalSampling, const bool relative, const bool normalized, + const void* _output_grad, const long long int* output_shape, + void* _input_grad, void* _lookup_grad, uint32_t* sample_count_buffer); +void SampleRGBAGridLuTGradKernelLauncher(const void* _input, const long long int* input_shape, + int32_t numCameras, + const float* _lookup, + const Sampling::CoordinateMode coordinateMode, + const Sampling::SamplerSettings samplingSettings, const bool globalSampling, const bool relative, const bool normalized, + const void* _output_grad, const long long int* output_shape, + void* _input_grad, void* _lookup_grad, uint32_t* sample_count_buffer); + +class SampleGridLuTGradOp : public OpKernel{ +public: + explicit SampleGridLuTGradOp(OpKernelConstruction *context) : OpKernel(context){ + + memset(&m_samplingSettings, 0, sizeof(Sampling::SamplerSettings)); + std::string s_interpolation; + OP_REQUIRES_OK(context, context->GetAttr("interpolation", &s_interpolation)); + if(s_interpolation.compare("LINEAR")==0) m_samplingSettings.filterMode = Sampling::SamplerSettings::FILTERMODE_LINEAR; + else if(s_interpolation.compare("MIN")==0) m_samplingSettings.filterMode = Sampling::SamplerSettings::FILTERMODE_MIN; + else if(s_interpolation.compare("MAX")==0) m_samplingSettings.filterMode = Sampling::SamplerSettings::FILTERMODE_MAX; + std::string s_boundary; + OP_REQUIRES_OK(context, context->GetAttr("boundary", &s_boundary)); + if(s_boundary.compare("CLAMP")==0) m_samplingSettings.boundaryMode = Sampling::SamplerSettings::CLAMP; + if(s_boundary.compare("WRAP")==0) m_samplingSettings.boundaryMode = Sampling::SamplerSettings::WRAP; + if(s_boundary.compare("MIRROR")==0) m_samplingSettings.boundaryMode = Sampling::SamplerSettings::MIRROR; + + std::string s_mipmapping; + OP_REQUIRES_OK(context, context->GetAttr("mipmapping", &s_mipmapping)); + if(s_mipmapping.compare("NEAREST")==0) m_samplingSettings.mipMode = Sampling::SamplerSettings::MIPMODE_NEAREST; + else if(s_mipmapping.compare("LINEAR")==0) m_samplingSettings.mipMode = Sampling::SamplerSettings::MIPMODE_LINEAR; + + OP_REQUIRES_OK(context, context->GetAttr("num_mipmaps", &m_samplingSettings.mipLevel)); + OP_REQUIRES(context, m_samplingSettings.mipLevel>0 || m_samplingSettings.mipMode==Sampling::SamplerSettings::MIPMODE_NONE, + errors::InvalidArgument("when using mipmaps num_mipmaps must be larger than 0.")); + + OP_REQUIRES_OK(context, context->GetAttr("mip_bias", &m_samplingSettings.mipBias)); + + OP_REQUIRES_OK(context, context->GetAttr("separate_camera_batch", &m_globalSampling)); + OP_REQUIRES_OK(context, context->GetAttr("relative_coords", &m_relativeCoords)); + OP_REQUIRES_OK(context, context->GetAttr("normalized_coords", &m_normalizedCoords)); + std::string s_coordmode; + OP_REQUIRES_OK(context, context->GetAttr("coordinate_mode", &s_coordmode)); + OP_REQUIRES(context, setCoordinateMode(m_coordinateMode, s_coordmode), + errors::InvalidArgument("invalid coordinate_mode.")); + OP_REQUIRES_OK(context, context->GetAttr("cell_center_offset", &m_samplingSettings.cellCenterOffset)); + if(m_normalizedCoords){ + OP_REQUIRES(context, m_samplingSettings.cellCenterOffset==0.5f, + errors::InvalidArgument("Invalid cell center offset must be 0.5 when using normalized coordinates.")); + } + } + + void Compute(OpKernelContext *context) override{ + + MYLOG("Gradient kernel start"); + + const Tensor& input_grid = context->input(0); + const Tensor& output_grad_grid = context->input(1); +// const Tensor& tensor_M = context->input(2); +// const Tensor& tensor_V = context->input(3); +// const Tensor& tensor_P = context->input(4); +// const Tensor& frustum = context->input(5); + const Tensor& tensor_lookup = context->input(2); + + //check input + MYLOG("Check input"); + TensorShape input_shape = input_grid.shape(); + OP_REQUIRES(context, input_grid.dims()==5 && input_shape.dim_size(4)<=4, + errors::InvalidArgument("Invalid input shape (NDHWC):", input_shape.DebugString())); + const int64 batch = input_shape.dim_size(0); + const int64 channel = input_shape.dim_size(4); + + //check output gradients + MYLOG("Check output_grads"); + TensorShape output_shape = output_grad_grid.shape(); + OP_REQUIRES(context, output_grad_grid.dims()==6 && output_shape.dim_size(5)<=4, + errors::InvalidArgument("Invalid output_grads shape (NVDHWC):", output_shape.DebugString())); + OP_REQUIRES(context, output_shape.dim_size(0)==batch, + errors::InvalidArgument("output_grads batch size does not match input batch size.")); + + + //check transform matricies + int32_t numCameras=0; + MYLOG("Check LuT"); + TensorShape lut_shape = tensor_lookup.shape(); + OP_REQUIRES(context, lut_shape.dims()==5 && lut_shape.dim_size(4)==4 + && lut_shape.dim_size(1)==output_shape.dim_size(2) + && lut_shape.dim_size(2)==output_shape.dim_size(3) + && lut_shape.dim_size(3)==output_shape.dim_size(4), + errors::InvalidArgument("Invalid lut shape (VDHWC):", lut_shape.DebugString(), ", DHW must macht output shape:", output_shape.DebugString())); + numCameras = lut_shape.dim_size(0); + //} + if(!m_globalSampling){ + OP_REQUIRES(context, numCameras==batch, errors::InvalidArgument("camera batch must match data batch when not using global sampling.")); + + OP_REQUIRES(context, output_shape.dim_size(1)==1, + errors::InvalidArgument("output_grads views size must be 1 if not using global sampling.")); + }else{ + OP_REQUIRES(context, output_shape.dim_size(1)==numCameras, + errors::InvalidArgument("output_grads views size does not match cameras.")); + } + + uint32_t* sample_count_buffer = nullptr; + Tensor sample_count_tensor; + if(NORMALIZE_GRADIENTS){ + MYLOG("Allocate gradient counter"); + TensorShape sample_count_tensor_shape; + sample_count_tensor_shape.AddDim(input_shape.dim_size(1)); + sample_count_tensor_shape.AddDim(input_shape.dim_size(2)); + sample_count_tensor_shape.AddDim(input_shape.dim_size(3)); + OP_REQUIRES_OK(context, context->allocate_temp(DT_UINT32, sample_count_tensor_shape, &sample_count_tensor)); + sample_count_buffer = sample_count_tensor.flat().data(); + } + + //allocate outout + MYLOG("Allocate input gradients"); + //OP_REQUIRES(context, output_shape.IsValid(), errors::InvalidArgument("Broken output shape")); + Tensor* input_grads = nullptr; + OP_REQUIRES_OK(context, context->allocate_output(0, input_shape, &input_grads)); + + Tensor* lookup_grads = nullptr; + OP_REQUIRES_OK(context, context->allocate_output(1, lut_shape, &lookup_grads)); + + + MYLOG("Resample\n"); + switch(channel){ + case 1: + SampleRGridLuTGradKernelLauncher(input_grid.flat().data(), input_shape.dim_sizes().data(), + numCameras, + tensor_lookup.flat().data(), + m_coordinateMode, + m_samplingSettings, m_globalSampling, m_relativeCoords, m_normalizedCoords, + output_grad_grid.flat().data(), output_shape.dim_sizes().data(), + input_grads->flat().data(), lookup_grads->flat().data(), sample_count_buffer); + break; + case 2: + SampleRGGridLuTGradKernelLauncher(input_grid.flat().data(), input_shape.dim_sizes().data(), + numCameras, + tensor_lookup.flat().data(), + m_coordinateMode, + m_samplingSettings, m_globalSampling, m_relativeCoords, m_normalizedCoords, + output_grad_grid.flat().data(), output_shape.dim_sizes().data(), + input_grads->flat().data(), lookup_grads->flat().data(), sample_count_buffer); + break; + case 4: + SampleRGBAGridLuTGradKernelLauncher(input_grid.flat().data(), input_shape.dim_sizes().data(), + numCameras, + tensor_lookup.flat().data(), + m_coordinateMode, + m_samplingSettings, m_globalSampling, m_relativeCoords, m_normalizedCoords, + output_grad_grid.flat().data(), output_shape.dim_sizes().data(), + input_grads->flat().data(), lookup_grads->flat().data(), sample_count_buffer); + break; + default: + OP_REQUIRES(context, false, + errors::Unimplemented("Only 1,2 and 4 Channel supported.")); + } + //*/ + MYLOG("Gradient kernel done"); + } +private: + bool m_globalSampling; + bool m_relativeCoords; + bool m_normalizedCoords; + //Sampling::SamplingMode m_samplingMode; + Sampling::SamplerSettings m_samplingSettings; + Sampling::CoordinateMode m_coordinateMode; +}; + +void ComputeLoDKernelLauncher(const long long int* input_shape, + const float* MV, const float* P, const float* _frustum, + const Sampling::CoordinateMode coordinateMode, + const bool relative, + void* output, const long long int* output_shape); + +class LoDTransformOp : public OpKernel{ +public: + explicit LoDTransformOp(OpKernelConstruction *context) : OpKernel(context){ + + bool inverseTransform; + bool linearizeDepth; + OP_REQUIRES_OK(context, context->GetAttr("inverse_transform", &inverseTransform)); + OP_REQUIRES_OK(context, context->GetAttr("linearize_depth", &linearizeDepth)); + m_coordinateMode = linearizeDepth + ? (inverseTransform ? Sampling::TransformLinDepthReverse : Sampling::TransformLinDepth) + : (inverseTransform ? Sampling::TransformReverse : Sampling::Transform); + + OP_REQUIRES_OK(context, context->GetAttr("relative_coords", &m_relativeCoords)); + } + + void Compute(OpKernelContext *context) override{ + + MYLOG("Compute LoD transform op kernel start"); + + const Tensor& input_shape_tensor = context->input(0); + const Tensor& tensor_MV = context->input(1); + const Tensor& tensor_P = context->input(2); + const Tensor& frustum = context->input(3); + const Tensor& output_shape_tensor = context->input(4); + + const int64 channel = 4; + + //check transform matrics + MYLOG("Check transform"); + int32 numMatrix_MV=0, numMatrix_P=0, numFrustum=0; + OP_REQUIRES(context, isValidTransformMatrix(tensor_MV.shape(), numMatrix_MV), + errors::InvalidArgument("transformation matrix_mv must be a 4x4 square matrix or a batch of 4x4 square matrices:", tensor_MV.shape().DebugString())); + OP_REQUIRES(context, isValidTransformMatrix(tensor_P.shape(), numMatrix_P), + errors::InvalidArgument("transformation matrix_p must be a 4x4 square matrix or a batch of 4x4 square matrices:", tensor_P.shape().DebugString())); + OP_REQUIRES(context, isValidFrustumParams(frustum.shape(), numFrustum), + errors::InvalidArgument("frustum must be a 1D tensor of 6 elements or a batch thereof:", frustum.shape().DebugString())); + //TODO + OP_REQUIRES(context, numMatrix_MV==numMatrix_P && numMatrix_MV==numFrustum, errors::InvalidArgument("camera batch size mismatch")); + const int64 batch = numMatrix_MV; + + //check input shape + MYLOG("Check input shape tensor"); + OP_REQUIRES(context, input_shape_tensor.dims()==1 && input_shape_tensor.dim_size(0)==3, errors::InvalidArgument("Invalid input_shape")); + MYLOG("Create input shape"); + TensorShape input_shape; + OP_REQUIRES_OK(context, makeShapeFromTensor(input_shape_tensor, &input_shape)); + MYLOG("Check input shape"); + OP_REQUIRES(context, input_shape.dims()==3, + errors::InvalidArgument("Invalid input_shape")); + input_shape.InsertDim(0,batch); + input_shape.AddDim(channel); + MYLOG("input_shape: " << input_shape.dim_size(0) << ", " << input_shape.dim_size(1) << ", " << input_shape.dim_size(2) << ", " << input_shape.dim_size(3)); + + //check output shape + MYLOG("Check output shape tensor"); + OP_REQUIRES(context, output_shape_tensor.dims()==1 && output_shape_tensor.dim_size(0)==3, errors::InvalidArgument("Invalid output_shape")); + MYLOG("Create output shape"); + TensorShape output_shape; + OP_REQUIRES_OK(context, makeShapeFromTensor(output_shape_tensor, &output_shape)); + MYLOG("Check output shape"); + OP_REQUIRES(context, output_shape.dims()==3, + errors::InvalidArgument("Invalid output_shape")); + output_shape.InsertDim(0,batch); + output_shape.AddDim(channel); + MYLOG("output_shape: " << output_shape.dim_size(0) << ", " << output_shape.dim_size(1) << ", " << output_shape.dim_size(2) << ", " << output_shape.dim_size(3)); + + + //allocate outout + MYLOG("Allocate output"); + //OP_REQUIRES(context, output_shape.IsValid(), errors::InvalidArgument("Broken output shape")); + Tensor* output_grid = NULL; + OP_REQUIRES_OK(context, context->allocate_output(0, output_shape, &output_grid)); + MYLOG("Check allocated output\n"); + auto out = output_grid->flat(); + MYLOG("Allocated output size: " << out.size() << " - " << output_grid->NumElements()); + + MYLOG("Resample"); + ComputeLoDKernelLauncher(input_shape.dim_sizes().data(), + tensor_MV.flat().data(), tensor_P.flat().data(), frustum.flat().data(), + m_coordinateMode, //m_inverseTransform? Sampling::TransformLinDepthReverse : Sampling::TransformLinDepth, + m_relativeCoords, + output_grid->flat().data(), output_shape.dim_sizes().data()); + + MYLOG("Kernel done"); + } +private: + bool m_relativeCoords; + Sampling::CoordinateMode m_coordinateMode; + //bool m_inverseTransform; +}; +#define REGISTER__CPU(T) + +#undef REGISTER__CPU + +#if GOOGLE_CUDA +#define REGISTER_GPU(T) \ + REGISTER_KERNEL_BUILDER(Name("SampleGridTransform") \ + .Device(DEVICE_GPU) \ + .TypeConstraint("T") \ + .HostMemory("matrix_m") \ + .HostMemory("matrix_v") \ + .HostMemory("matrix_p") \ + .HostMemory("frustum_params") \ + .HostMemory("output_shape") \ + , SampleGridTransformOp); +REGISTER_GPU(float); +#undef REGISTER_GPU +//REGISTER_KERNEL_BUILDER(Name("ResampleGridTransform").Device(DEVICE_CPU), ResampleGridTransformOp); +REGISTER_KERNEL_BUILDER(Name("SampleGridTransformGrad") \ + .Device(DEVICE_GPU) \ + .HostMemory("matrix_m") \ + .HostMemory("matrix_v") \ + .HostMemory("matrix_p") \ + .HostMemory("frustum_params") \ + , SampleGridTransformGradOp); + +REGISTER_KERNEL_BUILDER(Name("SampleGridLut") \ + .Device(DEVICE_GPU) \ + , SampleGridLuTOp); + +REGISTER_KERNEL_BUILDER(Name("SampleGridLutGrad") \ + .Device(DEVICE_GPU) \ + , SampleGridLuTGradOp); + +REGISTER_KERNEL_BUILDER(Name("LodTransform") \ + .Device(DEVICE_GPU) \ + .HostMemory("input_shape") \ + .HostMemory("matrix_mv") \ + .HostMemory("matrix_p") \ + .HostMemory("output_shape") \ + , LoDTransformOp); + +#endif //GOOGLE_CUDA \ No newline at end of file diff --git a/phitest/render/cuda/src/resample_grid.cu.cc b/phitest/render/cuda/src/resample_grid.cu.cc new file mode 100644 index 0000000..0c84860 --- /dev/null +++ b/phitest/render/cuda/src/resample_grid.cu.cc @@ -0,0 +1,1788 @@ + +#if GOOGLE_CUDA +#define EIGEN_USE_GPU +//#include "tensorflow/core/lib/core/errors.h" +//#include "tensorflow/core/platform/errors.h" +//#include "tensorflow/core/framework/op_kernel.h" +#include +#include "cuda-samples/Common/helper_cuda.h" +#include +#include +#include "resample_grid.hpp" +#include "render_errors.hpp" +//#define LOGGING + +#ifdef LOGGING +#define PROFILING +#endif + +#ifdef PROFILING +//#include +#include +#endif + +/* +#include "glm/mat4x4.hpp" +#include "glm/vec3.hpp" +#include "glm/vec4.hpp" +#include +*/ +#define GLM_ENABLE_EXPERIMENTAL +#include + +//#include //operators for cuda vector types +#include "vectormath_helper.hpp" +#include "vector_io.hpp" + +//kernel_setup params +#define BLOCK_SIZE_X 16 +#define BLOCK_SIZE_Y 4 +#define BLOCK_SIZE_Z 4 +//#define BLOCK_SIZE BLOCK_SIZE_X*BLOCK_SIZE_Y*BLOCK_SIZE_Z +//#define BLOCK_DIMS BLOCK_SIZE_X, BLOCK_SIZE_Y, BLOCK_SIZE_Z +#define CBUF_DIMENSIONS +#define CBUF_DIMENSIONS_INVERSE +#include "kernel_setup.hpp" + +#define CBUF_TRANSFORM_INVERSE +#define CBUF_FRUSTUM +#include "transformations.hpp" +#include "sampling.hpp" + +#define MIPGEN_BLOCK_X 8 +#define MIPGEN_BLOCK_Y 2 +#define MIPGEN_BLOCK_Z 2 + +//using GPUDevice = Eigen::GpuDevice; + +template +__global__ void +__launch_bounds__((MIPGEN_BLOCK_X*MIPGEN_BLOCK_Y*MIPGEN_BLOCK_Z)) +kGenerateMip3D(const T* UG_PTR input, T* UG_PTR output, glm::ivec3 dimensions){ + __shared__ T voxelBlock[MIPGEN_BLOCK_Z][MIPGEN_BLOCK_Y][MIPGEN_BLOCK_X]; + MAKE_GLOBAL_INDEX; + if(isInDimensions(globalIdx, dimensions)){ + voxelBlock[threadIdx.z][threadIdx.y][threadIdx.x] = vectorIO::readVectorType3D(globalIdx, dimensions, input); + }else{ + T def = {0}; + voxelBlock[threadIdx.z][threadIdx.y][threadIdx.x] = def; + } + __syncthreads(); + if((threadIdx.z&1)==0){ //(threadIdx.z%2)==0 even index + voxelBlock[threadIdx.z][threadIdx.y][threadIdx.x] = (voxelBlock[threadIdx.z][threadIdx.y][threadIdx.x] + voxelBlock[threadIdx.z+1][threadIdx.y][threadIdx.x])*0.5f; + } + __syncthreads(); + if((threadIdx.z&1)==0 && (threadIdx.y&1)==0){ + voxelBlock[threadIdx.z][threadIdx.y][threadIdx.x] = (voxelBlock[threadIdx.z][threadIdx.y][threadIdx.x] + voxelBlock[threadIdx.z][threadIdx.y+1][threadIdx.x])*0.5f; + } + __syncthreads(); + glm::ivec3 outIdx = globalIdx>>1; + glm::ivec3 outDims = dimensions>>1; + if((threadIdx.z&1)==0 && (threadIdx.y&1)==0 && (threadIdx.x&1)==0 && isInDimensions(outIdx, outDims)){ + vectorIO::writeVectorType3D((voxelBlock[threadIdx.z][threadIdx.y][threadIdx.x] + voxelBlock[threadIdx.z][threadIdx.y][threadIdx.x+1])*0.5f, outIdx, outDims, output); + } +} +//run on the (lower) input res and ADD to the 8 corresponding output elements +template +__global__ void +__launch_bounds__((MIPGEN_BLOCK_X*MIPGEN_BLOCK_Y*MIPGEN_BLOCK_Z)) +kCollapseGradMip3D(const T* UG_PTR input, T* UG_PTR output, glm::ivec3 dimensions){ + __shared__ T voxelBlock[MIPGEN_BLOCK_Z][MIPGEN_BLOCK_Y][MIPGEN_BLOCK_X]; + MAKE_GLOBAL_INDEX; + glm::ivec3 inDims = dimensions>>1; + T data = vectorIO::readVectorType3D(globalIdx, inDims, input); + glm::ivec3 outIdx = globalIdx<<1; + + glm::ivec3 offset = glm::ivec3(0); + #pragma unroll + for(; offset.z<2; ++offset.z){ + #pragma unroll + for(; offset.y<2; ++offset.y){ + #pragma unroll + for(; offset.x<2; ++offset.x){ + size_t idx = vectorIO::flatIdx3D(outIdx + offset, dimensions); + output[idx] += data; + } + } + } +} + +//use ALLOCATE_MIPS to have MipAtlas3D allocate memory for mip maps using cudaMalloc, otherwise provide your own buffer +//using this might cause issues when using tensorflow with default memory allocation +//#define ALLOCATE_MIPS + +#define ALIGN_UP(addr, align) (((addr) + (align-1) ) & ~(align-1)) +#define ALIGN_ADDR_UP(addr, align) ((reinterpret_cast(addr) + static_cast(align-1) ) & ~static_cast(align-1)) +template +class MipAtlas3D{ +public: + __host__ MipAtlas3D(const size_t level, const glm::ivec3 baseDims, const size_t textureAlignment=128) + : m_level(level), m_allocated(false), m_initialized(false), m_generated(false), m_baseDimensions(baseDims), m_baseLevel(nullptr), m_ptr(nullptr), m_alignment(textureAlignment){ + LOG("Init mip atlas with "<>m; + m_mipOffsetsBytes[m-1] = m_mipsSizeBytes; + m_mipsSizeBytes += ALIGN_UP(vmath::prod(m_dimensions[m])*sizeof(T), m_alignment); + } + m_totalSize = m_mipsSizeBytes + m_mipAtlasSizeBytes + sizeof(const T*); + + m_mips = new T*[m_level]; + //m_maps = new const T *[m_level+1]; + checkCudaErrors(cudaMallocHost(&m_maps_raw, m_mipAtlasSizeBytes + sizeof(const T*))); + m_maps = reinterpret_cast(ALIGN_ADDR_UP(m_maps_raw, sizeof(const T*))); + LOG("Mip atlas size "<(m_ptr); + }else{ + tmp_ptr = reinterpret_cast(ALIGN_ADDR_UP(buffer, m_alignment)); + m_ptr = buffer; + //tmp_ptr = reinterpret_cast(buffer); + } + + if(setZero){ + checkCudaErrors(cudaMemset(tmp_ptr, 0, m_totalSize)); + } + + for(int32_t m=0; m(tmp_ptr + m_mipOffsetsBytes[m]); + LOG("Mip "<(ALIGN_ADDR_UP(tmp_ptr + m_mipsSizeBytes, sizeof(const T*))); + LOG("device mip atlas at "<<<>>(m_maps[m], m_mips[m], m_dimensions[m]); +#ifdef PROFILING + CUDA_CHECK_RETURN(cudaDeviceSynchronize()); +#endif + m_maps[m+1] = m_mips[m]; + } + //copy here as currInput canges for each batch + if(async){ + LOG("Async copy "< +__global__ void +__launch_bounds__(BLOCK_SIZE) +kSample3D(Sampling::Sampler2 sampler, T* output){ + MAKE_GLOBAL_INDEX; + if(isInDimensions(globalIdx, c_dimensions.output)){ + glm::vec3 idxCoords = indexToCoords(glm::vec3(globalIdx)); + glm::vec4 samplePos = Sampling::samplePos3D(idxCoords); + T data = sampler.sample3D(samplePos); + //TODO improved depth correction (with binary depthCorrectionMask as parameter)? + vectorIO::writeVectorType3D(data, globalIdx, c_dimensions.output, output); + } +} +/*version to loop arbitrary channel and batches +* only works on simple data types, no vectors. no texture memory used +* shift thread index coordinates to channel, loop over z and batch: +* loop Db, loop Dz, Tz->Dy, Ty->Dx, Tx->Dc; for [b][z][y][x][c] data D and [z][y][x] threads T. +*/ //TODO test +/* +template +__global__ void +__launch_bounds__(BLOCK_SIZE) +kSample3DChannel(const T* input, T* output, const int32_t channel_dim, const int32_t batch_size){ + MAKE_GLOBAL_INDEX; + glm::ivec3 globalPos = glm::ivec3(globalIdx.y, globalIdx.z, 0); + + if(isInDimensions(globalPos, c_dimensions.output) && globalIdx.xbilinear interpolation!) + glm::vec3 samplePos = Sampling::samplePos3D(globalPos); + T data = Sampling::Sampler::sample3DChannel(input, samplePos, globalIdx.x, c_dimensions.input, channel_dim); + output[vectorIO::flatIdx3DChannel(globalPos, globalIdx.x, c_dimensions.output, channel_dim)] = data; + } + } + } +}*/ + +/* +* input: grid data to sample from, arr[b][z][y][x][c] layout, prod(input_shape) elements +* input_shape: shape of the input data, 4 elements (b,z,y,x) (more are ignored) +* MV: 4x4 model-view matrix; column major; x,y,z,w layout +* P: 4x4 projection matrix +* frustum: OpenGL view frustum parameters [near, far, left, right, top, bottom] +* lookup: grid with absolute positions to sample from, same dimensionality as output +* useLookup: wether to use the lookup grid or the (perspective) transformation as sample position +* globalSampling: wether sampling positions are the same for every batch +* output: grid to store output, arr[b][z][y][x][c] layout, prod(output_shape) elements +* output_shape: shape of the output data, 4 elements (b,z,y,x) +*/ +template +void SampleGridKernelLauncher(const GPUDevice& d, + const void* _input, const long long int* input_shape, + const float* M, const float* V, const float* P, const float* _frustum, int32_t numCameras, + // const float* _lookup, + uint8_t* _mipAtlas, + const Sampling::CoordinateMode coordinateMode, + const Sampling::SamplerSettings samplingSettings, const bool globalSampling, + void* _output, const long long int* output_shape){ + + LOG("Start SampleGridKernelLauncher"); +#ifdef PROFILING + CUDA_CHECK_RETURN(cudaDeviceSynchronize()); +#endif + + const T* input = reinterpret_cast(_input); + T* output = reinterpret_cast(_output); + /* + cudaDeviceProp prop; + cudaGetDeviceProperties(&prop, 0); + LOG("Device properties: " << prop.name); + LOG("\tCompute: " << prop.major << "." << prop.minor); + LOG("\tShared memory /block. " << prop.sharedMemPerBlock); + LOG("\tMax threads /block: " << prop.maxThreadsPerBlock << " (" << prop.maxThreadsDim[0] << "," << prop.maxThreadsDim[2] << "," << prop.maxThreadsDim[2] << ")"); + LOG("\tWarp size: " << prop.warpSize); + LOG("\tMem pitch: " << prop.memPitch); + //*/ + + + //precompute globals + BEGIN_SAMPLE; + LOG("Set dimensions"); + const size_t batchSize = input_shape[0]; + Dimensions dims; + setDimensions(dims, input_shape, output_shape+1); + LOG("Dimensions set"); + + + const size_t inputSliceSizeElements = vmath::prod(dims.input); + //const size_t texSizeBytes = inputSliceSizeElements*sizeof(T);//dims.input.x*sizeof(T)*dims.input.y*dims.input.z; + const size_t outputSliceSizeElements = vmath::prod(dims.output);//dims.output.x*dims.output.y*dims.output.z; + //const size_t outputSliceSizeBytes = outputSliceSizeElements*sizeof(T); + + const bool useMipmap=samplingSettings.mipMode != Sampling::SamplerSettings::MIPMODE_NONE;//Sampling::usesMip(samplingMode); + MipAtlas3D inputMips(samplingSettings.mipLevel, dims.input); + + //LOG("Set transformations"); + Transformations transforms; + FrustumParams frustum; + int32_t lastCamera=-1; + + END_SAMPLE("Precompute and copy global constants"); + + + Sampling::Sampler2 sampler; + memset(&sampler, 0, sizeof(Sampling::Sampler2)); + //tmp setup from old settings + sampler.settings = samplingSettings; + sampler.settings.mipClampMin = 0.f; + sampler.settings.mipClampMax = static_cast(samplingSettings.mipLevel); + //sampler.settings.mipLevel +=1; + sampler.dimensions = glm::ivec4(dims.input, sizeof(T)/sizeof(float)); //x,y,z,channel + //sampler.cellCenterOffset = coordinateMode!=Sampling::LuT? 0.5f : 0.0f; + + //LOG("filter mode: "<>m; + mipOffsetsBytes[m-1] = mipsSizeBytes; + mipsSizeBytes += ALIGN_ADDR_UP(vmath::prod(mipDimensions[m])*sizeof(T)); + } + +#ifdef ALLOCATE_MIPS + checkCudaErrors(cudaMalloc(&mipsAtlas, mipsSizeBytes + ALIGN_ADDR_UP(mipTableSizeBytes))); //is aligned + uint8_t* d_mipsStart = mipsAtlas; +#else //using tensorflow allocator instead + uint8_t* d_mipsStart = ALIGN_ADDR_UP(mipsAtlas); +#endif + + + checkCudaErrors(cudaMallocHost(&mips_raw, mipTableSizeBytes)); + mips = reinterpret_cast(mips_raw); + for(int32_t m=1; m<(mipLevels+1); ++m){ + mips[m] = reinterpret_cast(d_mipsStart + mipOffsetsBytes[m-1]); + } + + d_mips = reinterpret_cast(d_mipsStart + mipsSizeBytes); + sampler.d_mips = d_mips; + */ +#ifdef PROFILING + CUDA_CHECK_RETURN(cudaDeviceSynchronize()); +#endif + //#undef ALIGN_ADDR_UP + } + END_SAMPLE("Mipmap allocation"); + } + + /* //kernel grid and block setup for mip generation + dim3 mipGrid[mipLevels+1]; + const dim3 mipBlock(MIPGEN_BLOCK_X,MIPGEN_BLOCK_Y,MIPGEN_BLOCK_Z); + for(int32_t m=0; m(input+batch*inputSliceSizeElements); + if(useMipmap){ + BEGIN_SAMPLE; + { + sampler.d_mips = inputMips.getAtlas(currInput, d.stream()); + /* + mips[0] = currInput; + for(int32_t m=0; m<<>>(mips[m], mips[m+1], mipDimensions[m]); + } + //copy here as currInput canges for each batch + checkCudaErrors(cudaMemcpy(d_mips, mips, (mipLevels+1)*sizeof(T*), cudaMemcpyHostToDevice)); + //have to sync to not override mips[0] with next batch before this has completed... + //move set of d_mips into kGenerateMip3D? + //checkCudaErrors(cudaMemcpyAsync(d_mips, mips, (mipLevels+1)*sizeof(T*), cudaMemcpyHostToDevice, d.stream())); + */ +#ifdef PROFILING + CUDA_CHECK_RETURN(cudaDeviceSynchronize()); +#endif + } + END_SAMPLE("Mipmap generation"); + }else{ + sampler.d_input = currInput; + } + + size_t camera = globalSampling? 0 : batch; + size_t endCamera = globalSampling? numCameras : camera+1; + for(; camera<<>>(inputTexture, currLookup, currOutput); + //break; + case Sampling::TransformReverse: + if(useMipmap) kSample3D<<>>(sampler, currOutput); + else kSample3D<<>>(sampler, currOutput); + break; + case Sampling::Transform: + if(useMipmap) kSample3D<<>>(sampler, currOutput); + else kSample3D<<>>(sampler, currOutput); + break; + case Sampling::TransformLinDepthReverse: + if(useMipmap) kSample3D<<>>(sampler, currOutput); + else kSample3D<<>>(sampler, currOutput); + break; + case Sampling::TransformLinDepth: + if(useMipmap) kSample3D<<>>(sampler, currOutput); + else kSample3D<<>>(sampler, currOutput); + // break; + // case Sampling::LuT: kSample3DLuT<<>>(sampler, currOutput, currLookup); + break; + default: throw std::invalid_argument("Unsupported coordinate mode."); + } +#ifdef PROFILING + CUDA_CHECK_RETURN(cudaDeviceSynchronize()); +#endif + } + END_SAMPLE("Sample kernel"); + + /* + if(numCameras>1 || batch>1){ //only set new camera if there are multiple + //TODO update sampling params + if(coordinateMode!=Sampling::LuT){ + setTransformations(transforms, M + batch*16, V + camera*16, P + camera*16); + setFrustumParams(frustum, _frustum + camera*6); + }else{ + currLookup = lookup + camera*outputSliceSizeElements; + } + }*/ + } + } +#ifdef PROFILING + CUDA_CHECK_RETURN(cudaDeviceSynchronize()); +#endif + BEGIN_SAMPLE; + { + /* + if(useMipmap){ +#ifdef ALLOCATE_MIPS + cudaFree(mipsAtlas); +#endif + cudaFreeHost(mips_raw); + /* + cudaFree(d_mips); + for(int32_t m=1; m<(mipLevels+1); ++m){ + cudaFree(mips[m]); + } + * / + } + */ + } + END_SAMPLE("Free memory"); + CUDA_CHECK_RETURN(cudaDeviceSynchronize()); + LOG("End SampleGridKernelLauncher"); +} + +#define DEFINE_GPU_SPECS(T, C, VEC) \ + template<> \ + void SampleGridKernel::operator()(const GPUDevice& d, \ + const void* input, const long long int* input_shape, \ + const float* M, const float* V, const float* P, const float* frustum, int32_t numCameras, \ + uint8_t* mipAtlas, \ + const Sampling::CoordinateMode coordinateMode, \ + const Sampling::SamplerSettings samplingMode, const bool globalSampling, \ + void* output, const long long int* output_shape){ \ + SampleGridKernelLauncher(d, \ + input, input_shape, \ + M, V, P, frustum, numCameras, mipAtlas, \ + coordinateMode, \ + samplingMode, globalSampling, \ + output, output_shape); \ + } \ + template struct SampleGridKernel; +DEFINE_GPU_SPECS(float, 1, float1); +DEFINE_GPU_SPECS(float, 2, float2); +DEFINE_GPU_SPECS(float, 4, float4); + + +#undef DEFINE_GPU_SPECS + +/* +template +__global__ void +__launch_bounds__(BLOCK_SIZE) +kSample3DLuTMacCormackCorrection(Sampling::Sampler2 sampler, const T* input, const T* minVal, const T* maxVal, T* output, T* outputBounds, const float4* lookup, const bool relative=false, const bool normalized=false){ + MAKE_GLOBAL_INDEX; + if(isInDimensions(globalIdx, c_dimensions.output)){ + glm::vec4 samplePos = vectorIO::readVectorType3D(globalIdx, c_dimensions.output, lookup); + //TODO support for normalized and relative coordinates + if(normalized){//sampling coordinates are normalized to [0,1], this de-normalization assumes 0.5 center offset. + //There could be a center offset in sampler.settings.cellCenterOffset? + samplePos *= glm::vec4(c_dimensions.output, 1.0); + } + //glm::vec4 globalCoords(globalIdx, 0); + if(relative){//sampling coordines are relative to the output position, center offset does not matter + samplePos = glm::vec4(glm::vec3(globalIdx) - samplePos.xyz, samplePos.w); + } + T data_back = sampler.sample3D(samplePos); + T data_in = vectorIO::readVectorType3D(globalIdx, c_dimensions.input, input); + T data_fwd = vectorIO::readVectorType3D(globalIdx, c_dimensions.input, sampler.d_input); + T correction = data_fwd + (data_in - data_back)*0.5; + + //soft clamp + T data_min = vectorIO::readVectorType3D(globalIdx, c_dimensions.input, minVal); + T data_max = vectorIO::readVectorType3D(globalIdx, c_dimensions.input, maxVal); + T in_bounds = (data_min(data, globalIdx, c_dimensions.output, output); + vectorIO::writeVectorType3D(in_bounds, globalIdx, c_dimensions.output, outputBounds); + } +} +*/ + +template +__global__ void +__launch_bounds__(BLOCK_SIZE) +kSampleGrid3DLuT(const Sampling::SamplerSettings settings, const Sampling::Grid3D input, T* output, const float4* lookup, const bool relative, const bool normalized){ + MAKE_GLOBAL_INDEX; + if(isInDimensions(globalIdx, c_dimensions.output)){ + glm::vec4 samplePos = vectorIO::readVectorType3D(globalIdx, c_dimensions.output, lookup); + //support for normalized and relative coordinates + if(normalized){//sampling coordinates are normalized to [0,1], this de-normalization assumes 0.5 center offset. + //There could be a center offset in sampler.settings.cellCenterOffset? + samplePos *= glm::vec4(c_dimensions.output, 1.0); + } + if(relative){//sampling coordines are relative to the output position, center offset does not matter + samplePos += glm::vec4(globalIdx, 0.0); + } + T data = Sampling::sample3D(settings, input, samplePos); //sampler.sample3D(samplePos); + //TODO improved depth correction (with binary depthCorrectionMask as parameter)? + vectorIO::writeVectorType3D(data, globalIdx, c_dimensions.output, output); + }//*/ +} + +/* +* input: grid data to sample from, arr[b][z][y][x][c] layout, prod(input_shape) elements +* input_shape: shape of the input data, 4 elements (b,z,y,x) (more are ignored) +* MV: 4x4 model-view matrix; column major; x,y,z,w layout +* P: 4x4 projection matrix +* frustum: OpenGL view frustum parameters [near, far, left, right, top, bottom] +* lookup: grid with absolute positions to sample from, same dimensionality as output +* useLookup: wether to use the lookup grid or the (perspective) transformation as sample position +* globalSampling: wether sampling positions are the same for every batch +* output: grid to store output, arr[b][z][y][x][c] layout, prod(output_shape) elements +* output_shape: shape of the output data, 4 elements (b,z,y,x) +*/ +template +void SampleGridLuTKernelLauncher(const void* _input, const long long int* input_shape, + int32_t numCameras, + const float* _lookup, uint8_t* _mipAtlas, + const Sampling::CoordinateMode coordinateMode, + Sampling::SamplerSettings samplingSettings, const bool globalSampling, + const bool relative, const bool normalized, + void* _output, const long long int* output_shape){ + + LOG("Start SampleGridLuTKernelLauncher"); + + const float4* lookup = reinterpret_cast(_lookup); + const T* input = reinterpret_cast(_input); + T* output = reinterpret_cast(_output); + + //const bool useTextures=samplingSettings.useTexture;//Sampling::usesTexture(samplingMode); + const bool useMipmap=samplingSettings.mipMode != Sampling::SamplerSettings::MIPMODE_NONE;//Sampling::usesMip(samplingMode); + + //precompute globals + BEGIN_SAMPLE; + LOG("Set dimensions"); + const size_t batchSize = input_shape[0]; + Dimensions dims; + setDimensions(dims, input_shape, output_shape+1); + LOG("Dimensions set"); + + + const size_t inputSliceSizeElements = vmath::prod(dims.input); + //const size_t texSizeBytes = inputSliceSizeElements*sizeof(T);//dims.input.x*sizeof(T)*dims.input.y*dims.input.z; + const size_t outputSliceSizeElements = vmath::prod(dims.output);//dims.output.x*dims.output.y*dims.output.z; + //const size_t outputSliceSizeBytes = outputSliceSizeElements*sizeof(T); + + + END_SAMPLE("Precompute and copy global constants"); + + MipAtlas3D inputMips(samplingSettings.mipLevel, dims.input); + //tmp setup from old settings + samplingSettings.mipClampMin = 0.f; + samplingSettings.mipClampMax = static_cast(samplingSettings.mipLevel); + //samplingSettings.mipLevel +=1; + //sampler.dimensions = glm::ivec4(dims.input, sizeof(T)/sizeof(float)); //x,y,z,channel + //sampler.cellCenterOffset = coordinateMode!=Sampling::LuT? 0.5f : 0.0f; + + //LOG("filter mode: "< inputGrid; + memset(&inputGrid, 0, sizeof(Sampling::Grid3D)); + inputGrid.dimensions = glm::ivec4(dims.input, sizeof(T)/sizeof(float)); + inputGrid.dimensionsInverse = 1.0f/glm::vec3(dims.input); + inputGrid.mipLevel = 0; + + if(useMipmap){ + inputGrid.mipLevel = samplingSettings.mipLevel; + BEGIN_SAMPLE; + { +#ifdef ALLOCATE_MIPS + inputMips.initialize(nullptr, true); +#else + inputMips.initialize(_mipAtlas); +#endif +#ifdef PROFILING + CUDA_CHECK_RETURN(cudaDeviceSynchronize()); +#endif + } + END_SAMPLE("Mipmap allocation"); + } + + + int32_t lastCamera=-1; + const float4* currLookup = lookup; + + + const dim3 grid(GRID_DIMS(dims.output)); + const dim3 block(BLOCK_DIMS); + LOG("Sample " << batchSize << " grids with " << numCameras << " cameras"); + + for(size_t batch=0; batch(input+batch*inputSliceSizeElements); + if(useMipmap){ + BEGIN_SAMPLE; + { + inputMips.generate(currInput, 0); + inputGrid.d_mips = inputMips.getAtlas(); +#ifdef PROFILING + CUDA_CHECK_RETURN(cudaDeviceSynchronize()); +#endif + } + END_SAMPLE("Mipmap generation"); + }else{ + inputGrid.d_data = currInput; + } + + size_t camera = globalSampling? 0 : batch; + size_t endCamera = globalSampling? numCameras : camera+1; + for(; camera \ + <<>>(samplingSettings, inputGrid, currOutput, currLookup, relative, normalized) + KERNEL_SWITCH(NEAREST, NONE, BORDER); + else KERNEL_SWITCH(LINEAR, NONE, BORDER); + else KERNEL_SWITCH(MIN, NONE, BORDER); + else KERNEL_SWITCH(MAX, NONE, BORDER); + else KERNEL_SWITCH(NEAREST, NEAREST, BORDER); + else KERNEL_SWITCH(LINEAR, NEAREST, BORDER); + else KERNEL_SWITCH(MIN, NEAREST, BORDER); + else KERNEL_SWITCH(MAX, NEAREST, BORDER); + else KERNEL_SWITCH(NEAREST, LINEAR, BORDER); + else KERNEL_SWITCH(LINEAR, LINEAR, BORDER); + else KERNEL_SWITCH(MIN, LINEAR, BORDER); + else KERNEL_SWITCH(MAX, LINEAR, BORDER); + + else KERNEL_SWITCH(NEAREST, NONE, CLAMP); + else KERNEL_SWITCH(LINEAR, NONE, CLAMP); + else KERNEL_SWITCH(MIN, NONE, CLAMP); + else KERNEL_SWITCH(MAX, NONE, CLAMP); + else KERNEL_SWITCH(NEAREST, NEAREST, CLAMP); + else KERNEL_SWITCH(LINEAR, NEAREST, CLAMP); + else KERNEL_SWITCH(MIN, NEAREST, CLAMP); + else KERNEL_SWITCH(MAX, NEAREST, CLAMP); + else KERNEL_SWITCH(NEAREST, LINEAR, CLAMP); + else KERNEL_SWITCH(LINEAR, LINEAR, CLAMP); + else KERNEL_SWITCH(MIN, LINEAR, CLAMP); + else KERNEL_SWITCH(MAX, LINEAR, CLAMP); + + else KERNEL_SWITCH(NEAREST, NONE, WRAP); + else KERNEL_SWITCH(LINEAR, NONE, WRAP); + else KERNEL_SWITCH(MIN, NONE, WRAP); + else KERNEL_SWITCH(MAX, NONE, WRAP); + else KERNEL_SWITCH(NEAREST, NEAREST, WRAP); + else KERNEL_SWITCH(LINEAR, NEAREST, WRAP); + else KERNEL_SWITCH(MIN, NEAREST, WRAP); + else KERNEL_SWITCH(MAX, NEAREST, WRAP); + else KERNEL_SWITCH(NEAREST, LINEAR, WRAP); + else KERNEL_SWITCH(LINEAR, LINEAR, WRAP); + else KERNEL_SWITCH(MIN, LINEAR, WRAP); + else KERNEL_SWITCH(MAX, LINEAR, WRAP); + + else throw std::invalid_argument("Unsupported sampling configuration."); + #undef KERNEL_SWITCH + CUDA_CHECK_RETURN(cudaDeviceSynchronize()); + } + END_SAMPLE("Sample kernel"); + } + } + CUDA_CHECK_RETURN(cudaDeviceSynchronize()); + BEGIN_SAMPLE; + { + } + END_SAMPLE("Free memory"); + LOG("End SampleGridLuTKernelLauncher"); +} + +void SampleRGridLuTKernelLauncher( + const void* input, const long long int* input_shape, + int32_t numCameras, + const float* lookup, uint8_t* mipAtlas, + const Sampling::CoordinateMode coordinateMode, + Sampling::SamplerSettings samplingMode, const bool globalSampling, const bool relative, const bool normalized, + void* output, const long long int* output_shape){ + SampleGridLuTKernelLauncher(input, input_shape, + numCameras, lookup, mipAtlas, + coordinateMode, + samplingMode, globalSampling, relative, normalized, + output, output_shape); +} +void SampleRGGridLuTKernelLauncher( + const void* input, const long long int* input_shape, + int32_t numCameras, + const float* lookup, uint8_t* mipAtlas, + const Sampling::CoordinateMode coordinateMode, + Sampling::SamplerSettings samplingMode, const bool globalSampling, const bool relative, const bool normalized, + void* output, const long long int* output_shape){ + SampleGridLuTKernelLauncher(input, input_shape, + numCameras, lookup, mipAtlas, + coordinateMode, + samplingMode, globalSampling, relative, normalized, + output, output_shape); +} +void SampleRGBAGridLuTKernelLauncher( + const void* input, const long long int* input_shape, + int32_t numCameras, + const float* lookup, uint8_t* mipAtlas, + const Sampling::CoordinateMode coordinateMode, + Sampling::SamplerSettings samplingMode, const bool globalSampling, const bool relative, const bool normalized, + void* output, const long long int* output_shape){ + SampleGridLuTKernelLauncher(input, input_shape, + numCameras, lookup, mipAtlas, + coordinateMode, + samplingMode, globalSampling, relative, normalized, + output, output_shape); +} + +/******************************************** +* --- Backwards / Gradient Pass --- +********************************************/ + +//input and output retain their forward meaning (i.e. we compute output->input) + +/* binning no longer supported... */ + +/* +template +__device__ inline void atomicAddVectorType3D(const T v, const glm::ivec3 pos, const glm::ivec3 dims, T* buf); +template<> +__device__ inline void atomicAddVectorType3D(const float1 v, const glm::ivec3 pos, const glm::ivec3 dims, float1* buf){ + const size_t offset = vectorIO::flatIdx3D(pos, dims); + float * buf_raw = reinterpret_cast(buf + offset); + atomicAdd(buf_raw, v.x); +} +template<> +__device__ inline void atomicAddVectorType3D(const float2 v, const glm::ivec3 pos, const glm::ivec3 dims, float2* buf){ + const size_t offset = vectorIO::flatIdx3D(pos, dims); + float * buf_raw = reinterpret_cast(buf + offset); + atomicAdd(buf_raw, v.x); + atomicAdd(buf_raw +1, v.y); +} +template<> +__device__ inline void atomicAddVectorType3D(const float4 v, const glm::ivec3 pos, const glm::ivec3 dims, float4* buf){ + const size_t offset = vectorIO::flatIdx3D(pos, dims); + float * buf_raw = reinterpret_cast(buf + offset); + atomicAdd(buf_raw, v.x); + atomicAdd(buf_raw +1, v.y); + atomicAdd(buf_raw +2, v.z); + atomicAdd(buf_raw +3, v.w); +} +*/ +template +__device__ inline void atomicAddVectorType3D(const T v, const int32_t x, const int32_t y, const int32_t z, const glm::ivec3 dims, T* buf); +template<> +__device__ inline void atomicAddVectorType3D(const float1 v, const int32_t x, const int32_t y, const int32_t z, const glm::ivec3 dims, float1* buf){ + const size_t offset = vectorIO::flatIdx3D(x,y,z, dims); + float * buf_raw = reinterpret_cast(buf + offset); + atomicAdd(buf_raw, v.x); +} +template<> +__device__ inline void atomicAddVectorType3D(const float2 v, const int32_t x, const int32_t y, const int32_t z, const glm::ivec3 dims, float2* buf){ + const size_t offset = vectorIO::flatIdx3D(x,y,z, dims); + float * buf_raw = reinterpret_cast(buf + offset); + atomicAdd(buf_raw, v.x); + atomicAdd(buf_raw +1, v.y); +} +template<> +__device__ inline void atomicAddVectorType3D(const float4 v, const int32_t x, const int32_t y, const int32_t z, const glm::ivec3 dims, float4* buf){ + const size_t offset = vectorIO::flatIdx3D(x,y,z, dims); + float * buf_raw = reinterpret_cast(buf + offset); + atomicAdd(buf_raw, v.x); + atomicAdd(buf_raw +1, v.y); + atomicAdd(buf_raw +2, v.z); + atomicAdd(buf_raw +3, v.w); +} + +// position is without offset (+0.0) +template +__device__ void scatterGradQuadratic(const T out_grad, const glm::vec3 position, T* UG_PTR input_grad, uint32_t* UG_PTR num_samples){ + if(BM!=Sampling::SamplerSettings::BORDER || (CHECK_BOUNDS_SV3V3(-1.5f, <, position, <, c_dimensions.input))){ + glm::ivec3 centerIdx = glm::ivec3(glm::floor(position + 0.5f)); + glm::ivec3 prevIdx = centerIdx - 1; + glm::ivec3 nextIdx = centerIdx + 1; + + const glm::vec3 prevDist = glm::abs(position - glm::vec3(prevIdx)); + const glm::vec3 prevWeights = glm::vec3(Sampling::getQuadraticBSplineWeight(prevDist.x), Sampling::getQuadraticBSplineWeight(prevDist.y), Sampling::getQuadraticBSplineWeight(prevDist.z)); + + const glm::vec3 centerDist = glm::abs(position - glm::vec3(centerIdx)); + const glm::vec3 centerWeights = glm::vec3(Sampling::getQuadraticBSplineWeight(centerDist.x), Sampling::getQuadraticBSplineWeight(centerDist.y), Sampling::getQuadraticBSplineWeight(centerDist.z)); + + const glm::vec3 nextDist = glm::abs(position - glm::vec3(nextIdx)); + const glm::vec3 nextWeights = glm::vec3(Sampling::getQuadraticBSplineWeight(nextDist.x), Sampling::getQuadraticBSplineWeight(nextDist.y), Sampling::getQuadraticBSplineWeight(nextDist.z)); + + + if(BM == Sampling::SamplerSettings::CLAMP){ + prevIdx = glm::clamp(prevIdx, glm::ivec3(0), c_dimensions.input -1); + centerIdx = glm::clamp(centerIdx, glm::ivec3(0), c_dimensions.input -1); + nextIdx = glm::clamp(nextIdx, glm::ivec3(0), c_dimensions.input -1); + } + else if (BM == Sampling::SamplerSettings::WRAP){//periodic + prevIdx = vmath::positivemod(prevIdx, c_dimensions.input); + centerIdx = vmath::positivemod(centerIdx, c_dimensions.input); + nextIdx = vmath::positivemod(nextIdx, c_dimensions.input); + } + + //accumulate weighted gradients, xyz + //ppp + if(BM!=Sampling::SamplerSettings::BORDER || (prevIdx.x>=0 && prevIdx.y>=0 && prevIdx.z>=0)){ + atomicAddVectorType3D(out_grad*(prevWeights.x*prevWeights.y*prevWeights.z), prevIdx.x, prevIdx.y, prevIdx.z, c_dimensions.input, input_grad); + if(CountSamples){ atomicInc(num_samples + vectorIO::flatIdx3D(prevIdx.x, prevIdx.y, prevIdx.z, c_dimensions.input), 0xffffffff); } + } + //cpp + if(BM!=Sampling::SamplerSettings::BORDER || (prevIdx.y>=0 && prevIdx.z>=0)){ + atomicAddVectorType3D(out_grad*(centerWeights.x*prevWeights.y*prevWeights.z), centerIdx.x, prevIdx.y, prevIdx.z, c_dimensions.input, input_grad); + if(CountSamples){ atomicInc(num_samples + vectorIO::flatIdx3D(centerIdx.x, prevIdx.y, prevIdx.z, c_dimensions.input), 0xffffffff); } + } + //npp + if(BM!=Sampling::SamplerSettings::BORDER || (nextIdx.x=0 && prevIdx.z>=0)){ + atomicAddVectorType3D(out_grad*(nextWeights.x*prevWeights.y*prevWeights.z), nextIdx.x, prevIdx.y, prevIdx.z, c_dimensions.input, input_grad); + if(CountSamples){ atomicInc(num_samples + vectorIO::flatIdx3D(nextIdx.x, prevIdx.y, prevIdx.z, c_dimensions.input), 0xffffffff); } + } + + //pcp + if(BM!=Sampling::SamplerSettings::BORDER || (prevIdx.x>=0 && prevIdx.z>=0)){ + atomicAddVectorType3D(out_grad*(prevWeights.x*centerWeights.y*prevWeights.z), prevIdx.x, centerIdx.y, prevIdx.z, c_dimensions.input, input_grad); + if(CountSamples){ atomicInc(num_samples + vectorIO::flatIdx3D(prevIdx.x, centerIdx.y, prevIdx.z, c_dimensions.input), 0xffffffff); } + } + //ccp + if(BM!=Sampling::SamplerSettings::BORDER || (prevIdx.z>=0)){ + atomicAddVectorType3D(out_grad*(centerWeights.x*centerWeights.y*prevWeights.z), centerIdx.x, centerIdx.y, prevIdx.z, c_dimensions.input, input_grad); + if(CountSamples){ atomicInc(num_samples + vectorIO::flatIdx3D(centerIdx.x, centerIdx.y, prevIdx.z, c_dimensions.input), 0xffffffff); } + } + //ncp + if(BM!=Sampling::SamplerSettings::BORDER || (nextIdx.x=0)){ + atomicAddVectorType3D(out_grad*(nextWeights.x*centerWeights.y*prevWeights.z), nextIdx.x, centerIdx.y, prevIdx.z, c_dimensions.input, input_grad); + if(CountSamples){ atomicInc(num_samples + vectorIO::flatIdx3D(nextIdx.x, centerIdx.y, prevIdx.z, c_dimensions.input), 0xffffffff); } + } + + //pnp + if(BM!=Sampling::SamplerSettings::BORDER || (prevIdx.x>=0 && nextIdx.y=0)){ + atomicAddVectorType3D(out_grad*(prevWeights.x*nextWeights.y*prevWeights.z), prevIdx.x, nextIdx.y, prevIdx.z, c_dimensions.input, input_grad); + if(CountSamples){ atomicInc(num_samples + vectorIO::flatIdx3D(prevIdx.x, nextIdx.y, prevIdx.z, c_dimensions.input), 0xffffffff); } + } + //cnp + if(BM!=Sampling::SamplerSettings::BORDER || (nextIdx.y=0)){ + atomicAddVectorType3D(out_grad*(centerWeights.x*nextWeights.y*prevWeights.z), centerIdx.x, nextIdx.y, prevIdx.z, c_dimensions.input, input_grad); + if(CountSamples){ atomicInc(num_samples + vectorIO::flatIdx3D(centerIdx.x, nextIdx.y, prevIdx.z, c_dimensions.input), 0xffffffff); } + } + //nnp + if(BM!=Sampling::SamplerSettings::BORDER || (nextIdx.x=0)){ + atomicAddVectorType3D(out_grad*(nextWeights.x*nextWeights.y*prevWeights.z), nextIdx.x, nextIdx.y, prevIdx.z, c_dimensions.input, input_grad); + if(CountSamples){ atomicInc(num_samples + vectorIO::flatIdx3D(nextIdx.x, nextIdx.y, prevIdx.z, c_dimensions.input), 0xffffffff); } + } + + // ------ + //ppc + if(BM!=Sampling::SamplerSettings::BORDER || (prevIdx.x>=0 && prevIdx.y>=0)){ + atomicAddVectorType3D(out_grad*(prevWeights.x*prevWeights.y*centerWeights.z), prevIdx.x, prevIdx.y, centerIdx.z, c_dimensions.input, input_grad); + if(CountSamples){ atomicInc(num_samples + vectorIO::flatIdx3D(prevIdx.x, prevIdx.y, centerIdx.z, c_dimensions.input), 0xffffffff); } + } + //cpc + if(BM!=Sampling::SamplerSettings::BORDER || (prevIdx.y>=0)){ + atomicAddVectorType3D(out_grad*(centerWeights.x*prevWeights.y*centerWeights.z), centerIdx.x, prevIdx.y, centerIdx.z, c_dimensions.input, input_grad); + if(CountSamples){ atomicInc(num_samples + vectorIO::flatIdx3D(centerIdx.x, prevIdx.y, centerIdx.z, c_dimensions.input), 0xffffffff); } + } + //npc + if(BM!=Sampling::SamplerSettings::BORDER || (nextIdx.x=0)){ + atomicAddVectorType3D(out_grad*(nextWeights.x*prevWeights.y*centerWeights.z), nextIdx.x, prevIdx.y, centerIdx.z, c_dimensions.input, input_grad); + if(CountSamples){ atomicInc(num_samples + vectorIO::flatIdx3D(nextIdx.x, prevIdx.y, centerIdx.z, c_dimensions.input), 0xffffffff); } + } + + //pcc + if(BM!=Sampling::SamplerSettings::BORDER || (prevIdx.x>=0)){ + atomicAddVectorType3D(out_grad*(prevWeights.x*centerWeights.y*centerWeights.z), prevIdx.x, centerIdx.y, centerIdx.z, c_dimensions.input, input_grad); + if(CountSamples){ atomicInc(num_samples + vectorIO::flatIdx3D(prevIdx.x, centerIdx.y, centerIdx.z, c_dimensions.input), 0xffffffff); } + } + //ccc + if(BM!=Sampling::SamplerSettings::BORDER){ + atomicAddVectorType3D(out_grad*(centerWeights.x*centerWeights.y*centerWeights.z), centerIdx.x, centerIdx.y, centerIdx.z, c_dimensions.input, input_grad); + if(CountSamples){ atomicInc(num_samples + vectorIO::flatIdx3D(centerIdx.x, centerIdx.y, centerIdx.z, c_dimensions.input), 0xffffffff); } + } + //ncc + if(BM!=Sampling::SamplerSettings::BORDER || (nextIdx.x(out_grad*(nextWeights.x*centerWeights.y*centerWeights.z), nextIdx.x, centerIdx.y, centerIdx.z, c_dimensions.input, input_grad); + if(CountSamples){ atomicInc(num_samples + vectorIO::flatIdx3D(nextIdx.x, centerIdx.y, centerIdx.z, c_dimensions.input), 0xffffffff); } + } + + //pnc + if(BM!=Sampling::SamplerSettings::BORDER || (prevIdx.x>=0 && nextIdx.y(out_grad*(prevWeights.x*nextWeights.y*centerWeights.z), prevIdx.x, nextIdx.y, centerIdx.z, c_dimensions.input, input_grad); + if(CountSamples){ atomicInc(num_samples + vectorIO::flatIdx3D(prevIdx.x, nextIdx.y, centerIdx.z, c_dimensions.input), 0xffffffff); } + } + //cnc + if(BM!=Sampling::SamplerSettings::BORDER || (nextIdx.y(out_grad*(centerWeights.x*nextWeights.y*centerWeights.z), centerIdx.x, nextIdx.y, centerIdx.z, c_dimensions.input, input_grad); + if(CountSamples){ atomicInc(num_samples + vectorIO::flatIdx3D(centerIdx.x, nextIdx.y, centerIdx.z, c_dimensions.input), 0xffffffff); } + } + //nnc + if(BM!=Sampling::SamplerSettings::BORDER || (nextIdx.x(out_grad*(nextWeights.x*nextWeights.y*centerWeights.z), nextIdx.x, nextIdx.y, centerIdx.z, c_dimensions.input, input_grad); + if(CountSamples){ atomicInc(num_samples + vectorIO::flatIdx3D(nextIdx.x, nextIdx.y, centerIdx.z, c_dimensions.input), 0xffffffff); } + } + + // ------ + //ppn + if(BM!=Sampling::SamplerSettings::BORDER || (prevIdx.x>=0 && prevIdx.y>=0 && nextIdx.z(out_grad*(prevWeights.x*prevWeights.y*nextWeights.z), prevIdx.x, prevIdx.y, nextIdx.z, c_dimensions.input, input_grad); + if(CountSamples){ atomicInc(num_samples + vectorIO::flatIdx3D(prevIdx.x, prevIdx.y, nextIdx.z, c_dimensions.input), 0xffffffff); } + } + //cpn + if(BM!=Sampling::SamplerSettings::BORDER || (prevIdx.y>=0 && nextIdx.z(out_grad*(centerWeights.x*prevWeights.y*nextWeights.z), centerIdx.x, prevIdx.y, nextIdx.z, c_dimensions.input, input_grad); + if(CountSamples){ atomicInc(num_samples + vectorIO::flatIdx3D(centerIdx.x, prevIdx.y, nextIdx.z, c_dimensions.input), 0xffffffff); } + } + //npn + if(BM!=Sampling::SamplerSettings::BORDER || (nextIdx.x=0 && nextIdx.z(out_grad*(nextWeights.x*prevWeights.y*nextWeights.z), nextIdx.x, prevIdx.y, nextIdx.z, c_dimensions.input, input_grad); + if(CountSamples){ atomicInc(num_samples + vectorIO::flatIdx3D(nextIdx.x, prevIdx.y, nextIdx.z, c_dimensions.input), 0xffffffff); } + } + + //pcn + if(BM!=Sampling::SamplerSettings::BORDER || (prevIdx.x>=0 && nextIdx.z(out_grad*(prevWeights.x*centerWeights.y*nextWeights.z), prevIdx.x, centerIdx.y, nextIdx.z, c_dimensions.input, input_grad); + if(CountSamples){ atomicInc(num_samples + vectorIO::flatIdx3D(prevIdx.x, centerIdx.y, nextIdx.z, c_dimensions.input), 0xffffffff); } + } + //ccn + if(BM!=Sampling::SamplerSettings::BORDER || (nextIdx.z(out_grad*(centerWeights.x*centerWeights.y*nextWeights.z), centerIdx.x, centerIdx.y, nextIdx.z, c_dimensions.input, input_grad); + if(CountSamples){ atomicInc(num_samples + vectorIO::flatIdx3D(centerIdx.x, centerIdx.y, nextIdx.z, c_dimensions.input), 0xffffffff); } + } + //ncn + if(BM!=Sampling::SamplerSettings::BORDER || (nextIdx.x(out_grad*(nextWeights.x*centerWeights.y*nextWeights.z), nextIdx.x, centerIdx.y, nextIdx.z, c_dimensions.input, input_grad); + if(CountSamples){ atomicInc(num_samples + vectorIO::flatIdx3D(nextIdx.x, centerIdx.y, nextIdx.z, c_dimensions.input), 0xffffffff); } + } + + //pnn + if(BM!=Sampling::SamplerSettings::BORDER || (prevIdx.x>=0 && nextIdx.y(out_grad*(prevWeights.x*nextWeights.y*nextWeights.z), prevIdx.x, nextIdx.y, nextIdx.z, c_dimensions.input, input_grad); + if(CountSamples){ atomicInc(num_samples + vectorIO::flatIdx3D(prevIdx.x, nextIdx.y, nextIdx.z, c_dimensions.input), 0xffffffff); } + } + //cnn + if(BM!=Sampling::SamplerSettings::BORDER || (nextIdx.y(out_grad*(centerWeights.x*nextWeights.y*nextWeights.z), centerIdx.x, nextIdx.y, nextIdx.z, c_dimensions.input, input_grad); + if(CountSamples){ atomicInc(num_samples + vectorIO::flatIdx3D(centerIdx.x, nextIdx.y, nextIdx.z, c_dimensions.input), 0xffffffff); } + } + //nnn + if(BM!=Sampling::SamplerSettings::BORDER || (nextIdx.x(out_grad*(nextWeights.x*nextWeights.y*nextWeights.z), nextIdx.x, nextIdx.y, nextIdx.z, c_dimensions.input, input_grad); + if(CountSamples){ atomicInc(num_samples + vectorIO::flatIdx3D(nextIdx.x, nextIdx.y, nextIdx.z, c_dimensions.input), 0xffffffff); } + } + } +} +// position is without offset (+0.0) +template +__device__ void scatterGradInterpolated(const T out_grad, const glm::vec3 position, T* UG_PTR input_grad, uint32_t* UG_PTR num_samples){ + if(BM!=Sampling::SamplerSettings::BORDER || (CHECK_BOUNDS_SV3V3(-1.f, <, position, <, c_dimensions.input))){ + const glm::vec3 cw = glm::fract(position); + const glm::vec3 fw = 1.f - cw; + glm::ivec3 ceilIdx = glm::ivec3(glm::ceil(position)); + glm::ivec3 floorIdx = glm::ivec3(glm::floor(position)); + + if(BM==Sampling::SamplerSettings::CLAMP){ + ceilIdx = glm::clamp(ceilIdx, glm::ivec3(0), c_dimensions.input -1); + floorIdx = glm::clamp(floorIdx, glm::ivec3(0), c_dimensions.input -1); + }else if(BM==Sampling::SamplerSettings::WRAP){ + ceilIdx = vmath::positivemod(ceilIdx, c_dimensions.input); + floorIdx = vmath::positivemod(floorIdx, c_dimensions.input); + } + + //accumulate weighted gradients + if(BM!=Sampling::SamplerSettings::BORDER || (floorIdx.x>=0 && floorIdx.y>=0 && floorIdx.z>=0)){ + atomicAddVectorType3D(out_grad*(fw.x*fw.y*fw.z), floorIdx.x, floorIdx.y, floorIdx.z, c_dimensions.input, input_grad); + if(CountSamples){ atomicInc(num_samples + vectorIO::flatIdx3D(floorIdx.x, floorIdx.y, floorIdx.z, c_dimensions.input), 0xffffffff); } + } + if(BM!=Sampling::SamplerSettings::BORDER || (ceilIdx.x=0 && floorIdx.z>=0)){ + atomicAddVectorType3D(out_grad*(cw.x*fw.y*fw.z), ceilIdx.x, floorIdx.y, floorIdx.z, c_dimensions.input, input_grad); + if(CountSamples){ atomicInc(num_samples + vectorIO::flatIdx3D(ceilIdx.x, floorIdx.y, floorIdx.z, c_dimensions.input), 0xffffffff); } + } + if(BM!=Sampling::SamplerSettings::BORDER || (floorIdx.x>=0 && ceilIdx.y=0)){ + atomicAddVectorType3D(out_grad*(fw.x*cw.y*fw.z), floorIdx.x, ceilIdx.y, floorIdx.z, c_dimensions.input, input_grad); + if(CountSamples){ atomicInc(num_samples + vectorIO::flatIdx3D(floorIdx.x, ceilIdx.y, floorIdx.z, c_dimensions.input), 0xffffffff); } + } + if(BM!=Sampling::SamplerSettings::BORDER || (ceilIdx.x=0)){ + atomicAddVectorType3D(out_grad*(cw.x*cw.y*fw.z), ceilIdx.x, ceilIdx.y, floorIdx.z, c_dimensions.input, input_grad); + if(CountSamples){ atomicInc(num_samples + vectorIO::flatIdx3D(ceilIdx.x, ceilIdx.y, floorIdx.z, c_dimensions.input), 0xffffffff); } + } + + if(BM!=Sampling::SamplerSettings::BORDER || (floorIdx.x>=0 && floorIdx.y>=0 && ceilIdx.z(out_grad*(fw.x*fw.y*cw.z), floorIdx.x, floorIdx.y, ceilIdx.z, c_dimensions.input, input_grad); + if(CountSamples){ atomicInc(num_samples + vectorIO::flatIdx3D(floorIdx.x, floorIdx.y, ceilIdx.z, c_dimensions.input), 0xffffffff); } + } + if(BM!=Sampling::SamplerSettings::BORDER || (ceilIdx.x=0 && ceilIdx.z(out_grad*(cw.x*fw.y*cw.z), ceilIdx.x, floorIdx.y, ceilIdx.z, c_dimensions.input, input_grad); + if(CountSamples){ atomicInc(num_samples + vectorIO::flatIdx3D(ceilIdx.x, floorIdx.y, ceilIdx.z, c_dimensions.input), 0xffffffff); } + } + if(BM!=Sampling::SamplerSettings::BORDER || (floorIdx.x>=0 && ceilIdx.y(out_grad*(fw.x*cw.y*cw.z), floorIdx.x, ceilIdx.y, ceilIdx.z, c_dimensions.input, input_grad); + if(CountSamples){ atomicInc(num_samples + vectorIO::flatIdx3D(floorIdx.x, ceilIdx.y, ceilIdx.z, c_dimensions.input), 0xffffffff); } + } + if(BM!=Sampling::SamplerSettings::BORDER || (ceilIdx.x(out_grad*(cw.x*cw.y*cw.z), ceilIdx.x, ceilIdx.y, ceilIdx.z, c_dimensions.input, input_grad); + if(CountSamples){ atomicInc(num_samples + vectorIO::flatIdx3D(ceilIdx.x, ceilIdx.y, ceilIdx.z, c_dimensions.input), 0xffffffff); } + } + } +} + +template +__global__ void +__launch_bounds__(BLOCK_SIZE) +kNormalize3DGradients(T* UG_PTR grad, uint32_t* UG_PTR num_samples){ + MAKE_GLOBAL_INDEX; + if(isInDimensions(globalIdx, c_dimensions.input)){ + const size_t flatIdx = vectorIO::flatIdx3D(globalIdx.x, globalIdx.y, globalIdx.z, c_dimensions.input); + const uint32_t n = num_samples[flatIdx]; + if(n>0){ + const float weight = 1.0f / static_cast(n); + T data = vectorIO::readVectorType(flatIdx, grad); + vectorIO::writeVectorType(data * weight, flatIdx, grad); + + if(SetZero){ + num_samples[flatIdx] = 0; + } + } + } +} + + +/*Alternate method: + *scatter gradients to original inputs the output was interpolated from + *using atomicAdd for floats + * + * TODO handling LoD/mipmapping: scatter gradients to correct mip(s) and combine them after +*/ +template +__global__ void +__launch_bounds__(BLOCK_SIZE) +kScatter3DGradients(Sampling::Sampler2 sampler, const T* UG_PTR output_grad, T* UG_PTR input_grad, uint32_t* UG_PTR num_samples){ + MAKE_GLOBAL_INDEX; + if(isInDimensions(globalIdx, c_dimensions.output)){ + const T out_grad = vectorIO::readVectorType3D(globalIdx, c_dimensions.output, output_grad); + const glm::vec3 idxCoords = indexToCoords(glm::vec3(globalIdx)); + const glm::vec4 samplePos = Sampling::samplePos3D(idxCoords); + const glm::vec3 sampleIdx = sampler.getSamplingPosition(samplePos); + + if(sampler.settings.filterMode==Sampling::SamplerSettings::FILTERMODE_QUADRATIC){ + if(sampler.settings.boundaryMode==Sampling::SamplerSettings::BORDER){ + scatterGradQuadratic(out_grad, sampleIdx, input_grad, num_samples); + }else if(sampler.settings.boundaryMode==Sampling::SamplerSettings::CLAMP){ + scatterGradQuadratic(out_grad, sampleIdx, input_grad, num_samples); + }else if(sampler.settings.boundaryMode==Sampling::SamplerSettings::WRAP){ + scatterGradQuadratic(out_grad, sampleIdx, input_grad, num_samples); + } + }else{ + if(sampler.settings.boundaryMode==Sampling::SamplerSettings::BORDER){ + scatterGradInterpolated(out_grad, sampleIdx, input_grad, num_samples); + }else if(sampler.settings.boundaryMode==Sampling::SamplerSettings::CLAMP){ + scatterGradInterpolated(out_grad, sampleIdx, input_grad, num_samples); + }else if(sampler.settings.boundaryMode==Sampling::SamplerSettings::WRAP){ + scatterGradInterpolated(out_grad, sampleIdx, input_grad, num_samples); + } + } + } + +} +#define SCATTER_GRADIENTS +//*/ + + +template +void SampleGridGradKernelLauncher(const void* _input, const long long int* input_shape, + const float* M, const float* V, const float* P, const float* _frustum, int32_t numCameras, + // const float* _lookup, + const Sampling::CoordinateMode coordinateMode, + const Sampling::SamplerSettings samplingSettings, const bool globalSampling, + const void* _output_grad, const long long int* output_shape, + void* _input_grad, uint32_t* sample_count_buffer){ //, void* _lookup_grad + + LOG("Start SampleGridGradKernelLauncher"); + + const T* input = reinterpret_cast(_input); + T* input_grad = reinterpret_cast(_input_grad); + +// const float4* lookup = reinterpret_cast(_lookup); +// float4* lookup_grad = reinterpret_cast(_lookup_grad); + + const T* output_grad = reinterpret_cast(_output_grad); + + //precompute globals + + BEGIN_SAMPLE; + LOG("Set dimensions"); + const size_t batchSize = input_shape[0]; + Dimensions dims; + setDimensions(dims, input_shape, output_shape+1); + LOG("Dimensions set"); + + + const size_t inputSliceSizeElements = vmath::prod(dims.input); + //const size_t texSizeBytes = inputSliceSizeElements*sizeof(T);//dims.input.x*sizeof(T)*dims.input.y*dims.input.z; + const size_t outputSliceSizeElements = vmath::prod(dims.output);//dims.output.x*dims.output.y*dims.output.z; + //const size_t outputSliceSizeBytes = outputSliceSizeElements*sizeof(T); + + //LOG("Set transformations"); + Transformations transforms; + FrustumParams frustum; + int32_t lastCamera=-1; + + END_SAMPLE("Precompute and copy global constants"); + + const bool useMipmap=false;//samplingSettings.mipMode != Sampling::SamplerSettings::MIPMODE_NONE;//Sampling::usesMip(samplingMode); + MipAtlas3D inputMips(samplingSettings.mipLevel, dims.input); + //MipAtlas3D input_gradMips; + + Sampling::Sampler2 sampler; + memset(&sampler, 0, sizeof(Sampling::Sampler2)); + //tmp setup from old settings + sampler.settings = samplingSettings; + //gradients for mipmapped sampling currently not supported + sampler.settings.mipMode = Sampling::SamplerSettings::MIPMODE_NONE; + //sampler.settings.mipClampMin = 0.f; + //sampler.settings.mipClampMax = static_cast(mipLevels); + //sampler.settings.mipLevel +=1; + sampler.dimensions = glm::ivec4(dims.input, sizeof(T)/sizeof(float)); + + // zero gradient buffers + BEGIN_SAMPLE; + { + checkCudaErrors(cudaMemset(input_grad, 0, inputSliceSizeElements*sizeof(T)*batchSize)); + //TODO recompile + checkCudaErrors(cudaMemset(sample_count_buffer, 0, inputSliceSizeElements*sizeof(uint32_t))); + // if(coordinateMode==Sampling::LuT){ + // checkCudaErrors(cudaMemset(lookup_grad, 0, outputSliceSizeElements*sizeof(float4)*numCameras)); + // } +#ifdef PROFILING + CUDA_CHECK_RETURN(cudaDeviceSynchronize()); +#endif + } + END_SAMPLE("Set gradient buffers zero"); + + // allocate mips +/* if(useMipmap){ + BEGIN_SAMPLE; + { + inputMips.initialize(allocate=true); + //sampler.d_mips = inputMips.dd_mips; + // allocate mips for gradients + //allocateMipAtlas3D(input_gradMips, samplingSettings.mipLevel, dims.input, true); +#ifdef PROFILING + CUDA_CHECK_RETURN(cudaDeviceSynchronize()); +#endif + } + END_SAMPLE("Mipmap allocation"); + }*/ + +// const float4* currLookup = lookup; +// float4* currLookup_grad = lookup_grad; + + const dim3 grid(GRID_DIMS(dims.output)); + const dim3 block(BLOCK_DIMS); + LOG("Sample " << batchSize << " grids with " << numCameras << " cameras"); + + for(size_t batch=0; batch(input+batch*inputSliceSizeElements); + T* currInput_grad = input_grad+batch*inputSliceSizeElements; +/* if(useMipmap){ + BEGIN_SAMPLE; + { + inputMips.generate(currInput); + sampler.d_mips = inputMips.getAtlas(); +#ifdef PROFILING + CUDA_CHECK_RETURN(cudaDeviceSynchronize()); +#endif + } + END_SAMPLE("Mipmap generation"); + }else*/{ + sampler.d_input = currInput; + } + + + size_t camera = globalSampling? 0 : batch; + size_t endCamera = globalSampling? numCameras : camera+1; + for(; camera<<>>( + sampler, currOutput_grad, currInput_grad, sample_count_buffer); + else kScatter3DGradients<<>>( + sampler, currOutput_grad, currInput_grad, sample_count_buffer); + break; + case Sampling::Transform: + if(useMipmap) kScatter3DGradients<<>>( + sampler, currOutput_grad, currInput_grad, sample_count_buffer); + else kScatter3DGradients<<>>( + sampler, currOutput_grad, currInput_grad, sample_count_buffer); + break; + case Sampling::TransformLinDepthReverse: + if(useMipmap) kScatter3DGradients<<>>( + sampler, currOutput_grad, currInput_grad, sample_count_buffer); + else kScatter3DGradients<<>>( + sampler, currOutput_grad, currInput_grad, sample_count_buffer); + break; + case Sampling::TransformLinDepth: + if(useMipmap) kScatter3DGradients<<>>( + sampler, currOutput_grad, currInput_grad, sample_count_buffer); + else kScatter3DGradients<<>>( + sampler, currOutput_grad, currInput_grad, sample_count_buffer); + break; + default: throw std::invalid_argument("Unsupported coordinate mode."); + } + + CUDA_CHECK_RETURN(cudaDeviceSynchronize()); + } + END_SAMPLE("Sample kernel gradients"); + + } + if(useMipmap){ + //TODO: fuse mips into output + //kCollapseGradMip3D<<<>>>(mip[m+1], mip[m], dimensions[m]); + } + if(NORMALIZE_GRADIENTS){ + BEGIN_SAMPLE; + { + const dim3 grad_grid(GRID_DIMS(dims.input)); + kNormalize3DGradients<<>>(currInput_grad, sample_count_buffer); + //checkCudaErrors(cudaMemset(sample_count_buffer, 0, inputSliceSizeElements*sizeof(uint32_t))); + } + END_SAMPLE("Grad normalize"); + } + } + + + BEGIN_SAMPLE; + { +// if(useMipmap){ + //freeMipAtlas3D(inputMips); + //freeMipAtlas3D(input_gradMips); +// } + } + END_SAMPLE("Free memory"); + + LOG("End SampleGridGradKernelLauncher"); +} + +void SampleRGridGradKernelLauncher(const void* _input, const long long int* input_shape, + const float* M, const float* V, const float* P, const float* _frustum, int32_t numCameras, + const Sampling::CoordinateMode coordinateMode, + const Sampling::SamplerSettings samplingSettings, const bool globalSampling, + const void* _output_grad, const long long int* output_shape, + void* _input_grad, uint32_t* sample_count_buffer){ + SampleGridGradKernelLauncher(_input, input_shape, M, V, P, _frustum, numCameras, coordinateMode, samplingSettings, globalSampling, _output_grad, output_shape, _input_grad, sample_count_buffer); +} +void SampleRGGridGradKernelLauncher(const void* _input, const long long int* input_shape, + const float* M, const float* V, const float* P, const float* _frustum, int32_t numCameras, + const Sampling::CoordinateMode coordinateMode, + const Sampling::SamplerSettings samplingSettings, const bool globalSampling, + const void* _output_grad, const long long int* output_shape, + void* _input_grad, uint32_t* sample_count_buffer){ + SampleGridGradKernelLauncher(_input, input_shape, M, V, P, _frustum, numCameras, coordinateMode, samplingSettings, globalSampling, _output_grad, output_shape, _input_grad, sample_count_buffer); +} +void SampleRGBAGridGradKernelLauncher(const void* _input, const long long int* input_shape, + const float* M, const float* V, const float* P, const float* _frustum, int32_t numCameras, + const Sampling::CoordinateMode coordinateMode, + const Sampling::SamplerSettings samplingSettings, const bool globalSampling, + const void* _output_grad, const long long int* output_shape, + void* _input_grad, uint32_t* sample_count_buffer){ + SampleGridGradKernelLauncher(_input, input_shape, M, V, P, _frustum, numCameras, coordinateMode, samplingSettings, globalSampling, _output_grad, output_shape, _input_grad, sample_count_buffer); +} + + +/* + * input (grad): [depth, height, width, channel (1-4)] + * lut (grad): [depth, height, width, channel (4)], channel: (abs_x, abs_y, abs_z, LoD) +https://github.com/tensorflow/tensorflow/blob/r1.12/tensorflow/contrib/resampler/kernels/resampler_ops_gpu.cu.cc +*/ +template //const T* UG_PTR input, const float4* UG_PTR lookup +__global__ void kScatterLuT3DGradients(const Sampling::SamplerSettings settings, const Sampling::Grid3D input, const float4* UG_PTR lookup, const T* UG_PTR output_grad, T* input_grad, float4* lookup_grad, const bool relative, const bool normalized, uint32_t* UG_PTR num_samples){ + MAKE_GLOBAL_INDEX; + if(isInDimensions(globalIdx, c_dimensions.output)){ + glm::vec4 samplePos = vectorIO::readVectorType3D(globalIdx, c_dimensions.output, lookup); + //TODO support for normalized and relative coordinates + if(normalized){//sampling coordinates are normalized to [0,1], this de-normalization assumes 0.5 center offset. + //There could be a center offset in sampler.settings.cellCenterOffset? + samplePos *= glm::vec4(c_dimensions.output, 1.0); + } + if(relative){//sampling coordines are relative to the output position, center offset does not matter + samplePos += glm::vec4(globalIdx, 0.0); + } + + //TODO improved depth correction (with binary depthCorrectionMask as parameter)? + const T out_grad = vectorIO::readVectorType3D(globalIdx, c_dimensions.output, output_grad); + + if(FM==Sampling::SamplerSettings::FILTERMODE_LINEAR){ + Sampling::DataGrad3D dataGrad = Sampling::sampleGrad3D(settings, input, samplePos); + // TODO make LoD grads? + const float4 lut_grad = make_float4(vmath::sum(out_grad*dataGrad.dx), vmath::sum(out_grad*dataGrad.dy), vmath::sum(out_grad*dataGrad.dz), 0.f); + //atomicAddVectorType3D(lut_grad, globalIdx, c_dimensions.output, lookup_grad); + vectorIO::writeVectorType3D(lut_grad, globalIdx, c_dimensions.output, lookup_grad); + }//else gradients are 0, so keep 0 (memset outside) + + //TODO: scattering should respect the filter mode. + const glm::vec3 sampleIdx = (samplePos - settings.cellCenterOffset);//sampler.getSamplingPosition(samplePos); + scatterGradInterpolated(out_grad, sampleIdx, input_grad, num_samples); + //NEAREST, MIN, MAX: send gradients only to the cell the fwd value was taken from + } +} + +template +void SampleGridLuTGradKernelLauncher(const void* _input, const long long int* input_shape, + int32_t numCameras, + const float* _lookup, + const Sampling::CoordinateMode coordinateMode, + const Sampling::SamplerSettings samplingSettings, const bool globalSampling, const bool relative, const bool normalized, + const void* _output_grad, const long long int* output_shape, + void* _input_grad, void* _lookup_grad, uint32_t* sample_count_buffer){ + + LOG("Start SampleGridLuTGradKernelLauncher"); + + const T* input = reinterpret_cast(_input); + T* input_grad = reinterpret_cast(_input_grad); + + const float4* lookup = reinterpret_cast(_lookup); + float4* lookup_grad = reinterpret_cast(_lookup_grad); + + const T* output_grad = reinterpret_cast(_output_grad); + + //precompute globals + + BEGIN_SAMPLE; + LOG("Set dimensions"); + const size_t batchSize = input_shape[0]; + Dimensions dims; + setDimensions(dims, input_shape, output_shape+1); + LOG("Dimensions set"); + + + const size_t inputSliceSizeElements = vmath::prod(dims.input); + //const size_t texSizeBytes = inputSliceSizeElements*sizeof(T);//dims.input.x*sizeof(T)*dims.input.y*dims.input.z; + const size_t outputSliceSizeElements = vmath::prod(dims.output);//dims.output.x*dims.output.y*dims.output.z; + //const size_t outputSliceSizeBytes = outputSliceSizeElements*sizeof(T); + + //LOG("Set transformations"); +// Transformations transforms; +// FrustumParams frustum; + int32_t lastCamera=-1; + + END_SAMPLE("Precompute and copy global constants"); + +// const bool useMipmap=false;//samplingSettings.mipMode != Sampling::SamplerSettings::MIPMODE_NONE;//Sampling::usesMip(samplingMode); + //MipAtlas3D inputMips(samplingSettings.mipLevel, dims.input); + //MipAtlas3D input_gradMips; + + + //tmp setup from old settings + //samplingSettings.mipClampMin = 0.f; + //samplingSettings.mipClampMax = 0.f;//static_cast(samplingSettings.mipLevel); + + //LOG("filter mode: "< inputGrid; + memset(&inputGrid, 0, sizeof(Sampling::Grid3D)); + inputGrid.dimensions = glm::ivec4(dims.input, sizeof(T)/sizeof(float)); + inputGrid.dimensionsInverse = 1.0f/glm::vec3(dims.input); + inputGrid.mipLevel = 0; + + // zero gradient buffers + BEGIN_SAMPLE; + { + checkCudaErrors(cudaMemset(input_grad, 0, inputSliceSizeElements*sizeof(T)*batchSize)); + // if(coordinateMode==Sampling::LuT){ + checkCudaErrors(cudaMemset(lookup_grad, 0, outputSliceSizeElements*sizeof(float4)*numCameras)); + // } +#ifdef PROFILING + CUDA_CHECK_RETURN(cudaDeviceSynchronize()); +#endif + } + END_SAMPLE("Set gradient buffers zero"); + + // allocate mips +/* if(useMipmap){ + BEGIN_SAMPLE; + { + inputMips.initialize(allocate=true); + //sampler.d_mips = inputMips.dd_mips; + // allocate mips for gradients + //allocateMipAtlas3D(input_gradMips, samplingSettings.mipLevel, dims.input, true); +#ifdef PROFILING + CUDA_CHECK_RETURN(cudaDeviceSynchronize()); +#endif + } + END_SAMPLE("Mipmap allocation"); + }//*/ + + const float4* currLookup = lookup; + float4* currLookup_grad = lookup_grad; + + const dim3 grid(GRID_DIMS(dims.output)); + const dim3 block(BLOCK_DIMS); + LOG("Sample " << batchSize << " grids with " << numCameras << " cameras"); + + for(size_t batch=0; batch(input+batch*inputSliceSizeElements); + T* currInput_grad = input_grad+batch*inputSliceSizeElements; +/* if(useMipmap){ + BEGIN_SAMPLE; + { + inputMips.generate(currInput); + inputGrid.d_mips = inputMips.getAtlas(); +#ifdef PROFILING + CUDA_CHECK_RETURN(cudaDeviceSynchronize()); +#endif + } + END_SAMPLE("Mipmap generation"); + }else//*/{ + inputGrid.d_data = currInput; + } + + + size_t camera = globalSampling? 0 : batch; + size_t endCamera = globalSampling? numCameras : camera+1; + for(; camera \ + <<>>(samplingSettings, inputGrid, currLookup, currOutput_grad, currInput_grad, currLookup_grad, relative, normalized, sample_count_buffer) + KERNEL_SWITCH(NEAREST, NONE, BORDER); + else KERNEL_SWITCH(LINEAR, NONE, BORDER); + else KERNEL_SWITCH(MIN, NONE, BORDER); + else KERNEL_SWITCH(MAX, NONE, BORDER); + else KERNEL_SWITCH(NEAREST, NEAREST, BORDER); + else KERNEL_SWITCH(LINEAR, NEAREST, BORDER); + else KERNEL_SWITCH(MIN, NEAREST, BORDER); + else KERNEL_SWITCH(MAX, NEAREST, BORDER); + else KERNEL_SWITCH(NEAREST, LINEAR, BORDER); + else KERNEL_SWITCH(LINEAR, LINEAR, BORDER); + else KERNEL_SWITCH(MIN, LINEAR, BORDER); + else KERNEL_SWITCH(MAX, LINEAR, BORDER); + + else KERNEL_SWITCH(NEAREST, NONE, CLAMP); + else KERNEL_SWITCH(LINEAR, NONE, CLAMP); + else KERNEL_SWITCH(MIN, NONE, CLAMP); + else KERNEL_SWITCH(MAX, NONE, CLAMP); + else KERNEL_SWITCH(NEAREST, NEAREST, CLAMP); + else KERNEL_SWITCH(LINEAR, NEAREST, CLAMP); + else KERNEL_SWITCH(MIN, NEAREST, CLAMP); + else KERNEL_SWITCH(MAX, NEAREST, CLAMP); + else KERNEL_SWITCH(NEAREST, LINEAR, CLAMP); + else KERNEL_SWITCH(LINEAR, LINEAR, CLAMP); + else KERNEL_SWITCH(MIN, LINEAR, CLAMP); + else KERNEL_SWITCH(MAX, LINEAR, CLAMP); + + else KERNEL_SWITCH(NEAREST, NONE, WRAP); + else KERNEL_SWITCH(LINEAR, NONE, WRAP); + else KERNEL_SWITCH(MIN, NONE, WRAP); + else KERNEL_SWITCH(MAX, NONE, WRAP); + else KERNEL_SWITCH(NEAREST, NEAREST, WRAP); + else KERNEL_SWITCH(LINEAR, NEAREST, WRAP); + else KERNEL_SWITCH(MIN, NEAREST, WRAP); + else KERNEL_SWITCH(MAX, NEAREST, WRAP); + else KERNEL_SWITCH(NEAREST, LINEAR, WRAP); + else KERNEL_SWITCH(LINEAR, LINEAR, WRAP); + else KERNEL_SWITCH(MIN, LINEAR, WRAP); + else KERNEL_SWITCH(MAX, LINEAR, WRAP); + + else throw std::invalid_argument("Unsupported sampling configuration."); + #undef KERNEL_SWITCH + CUDA_CHECK_RETURN(cudaDeviceSynchronize()); + } + END_SAMPLE("Sample kernel gradients"); + + } +// if(useMipmap){ + //TODO: fuse mips into output + //kCollapseGradMip3D<<<>>>(mip[m+1], mip[m], dimensions[m]); +// } + if(NORMALIZE_GRADIENTS){ + BEGIN_SAMPLE; + { + const dim3 grad_grid(GRID_DIMS(dims.input)); + kNormalize3DGradients<<>>(currInput_grad, sample_count_buffer); + //checkCudaErrors(cudaMemset(sample_count_buffer, 0, inputSliceSizeElements*sizeof(uint32_t))); + } + END_SAMPLE("Grad normalize"); + } + } + + BEGIN_SAMPLE; + { +// if(useMipmap){ + //freeMipAtlas3D(inputMips); + //freeMipAtlas3D(input_gradMips); +// } + } + END_SAMPLE("Free memory"); + + LOG("End SampleGridLuTGradKernelLauncher"); +} + +void SampleRGridLuTGradKernelLauncher(const void* _input, const long long int* input_shape, + int32_t numCameras, + const float* _lookup, + const Sampling::CoordinateMode coordinateMode, + const Sampling::SamplerSettings samplingSettings, const bool globalSampling, const bool relative, const bool normalized, + const void* _output_grad, const long long int* output_shape, + void* _input_grad, void* _lookup_grad, uint32_t* sample_count_buffer){ + SampleGridLuTGradKernelLauncher(_input, input_shape, numCameras, _lookup, coordinateMode, samplingSettings, globalSampling, relative, normalized, + _output_grad, output_shape, _input_grad, _lookup_grad, sample_count_buffer); +} +void SampleRGGridLuTGradKernelLauncher(const void* _input, const long long int* input_shape, + int32_t numCameras, + const float* _lookup, + const Sampling::CoordinateMode coordinateMode, + const Sampling::SamplerSettings samplingSettings, const bool globalSampling, const bool relative, const bool normalized, + const void* _output_grad, const long long int* output_shape, + void* _input_grad, void* _lookup_grad, uint32_t* sample_count_buffer){ + SampleGridLuTGradKernelLauncher(_input, input_shape, numCameras, _lookup, coordinateMode, samplingSettings, globalSampling, relative, normalized, + _output_grad, output_shape, _input_grad, _lookup_grad, sample_count_buffer); +} +void SampleRGBAGridLuTGradKernelLauncher(const void* _input, const long long int* input_shape, + int32_t numCameras, + const float* _lookup, + const Sampling::CoordinateMode coordinateMode, + const Sampling::SamplerSettings samplingSettings, const bool globalSampling, const bool relative, const bool normalized, + const void* _output_grad, const long long int* output_shape, + void* _input_grad, void* _lookup_grad, uint32_t* sample_count_buffer){ + SampleGridLuTGradKernelLauncher(_input, input_shape, numCameras, _lookup, coordinateMode, samplingSettings, globalSampling, relative, normalized, + _output_grad, output_shape, _input_grad, _lookup_grad, sample_count_buffer); +} + +/******************************************** +* Compute LoD +********************************************/ + +template +__global__ void +__launch_bounds__(BLOCK_SIZE) +kComputeLoD3D(float4* output, const bool relative){ + MAKE_GLOBAL_INDEX; + if(isInDimensions(globalIdx, c_dimensions.output)){ + glm::vec3 idxCoords = indexToCoords(glm::vec3(globalIdx)); + glm::vec4 sizeLoD = Sampling::calcLoD(idxCoords); + glm::vec4 pos = Sampling::samplePos3D(idxCoords); + if(relative){//sampling coordines are relative to the output position, center offset does not matter + pos -= glm::vec4(globalIdx, 0.0); + } + pos.w=sizeLoD.w; + vectorIO::writeVectorType3D(vectorIO::toVector(pos), globalIdx, c_dimensions.output, output); + } +} + +void ComputeLoDKernelLauncher(const long long int* input_shape, + const float* MV, const float* P, const float* _frustum, + const Sampling::CoordinateMode coordinateMode, + const bool relative, + void* output, const long long int* output_shape){ + + LOG("Start ComputeLoDKernelLauncher"); + + //precompute globals + BEGIN_SAMPLE; + LOG("Set dimensions"); + const size_t batchSize = input_shape[0]; + Dimensions dims; + setDimensions(dims, input_shape, output_shape); + LOG("Dimensions set"); + + const size_t outputSliceSizeElements = vmath::prod(dims.output);//dims.output.x*dims.output.y*dims.output.z; + //const size_t outputSliceSizeBytes = outputSliceSizeElements*sizeof(T); + + Transformations transforms; + FrustumParams frustum; + + END_SAMPLE("Precompute and copy global constants"); + + + const dim3 grid(GRID_DIMS(dims.output)); + const dim3 block(BLOCK_DIMS); + LOG("Generate sampling coords for " << batchSize << " grids"); + for(size_t batch=0; batch(output)+batch*outputSliceSizeElements; + + LOG("Set transformations"); + if(coordinateMode!=Sampling::LuT){ + setTransformations(transforms, nullptr, MV + batch*16, P + batch*16); + LOG("Transformations set"); + setFrustumParams(frustum, _frustum + batch*6); + LOG("FrustumParams set"); + } + + LOG("Dipatch CUDA kernel: " << dims.output.x << " x " << dims.output.y << " x " << dims.output.z << " threads"); + BEGIN_SAMPLE; + { + switch(coordinateMode){ + case Sampling::TransformReverse: kComputeLoD3D<<>>(currOutput, relative); + break; + case Sampling::Transform: kComputeLoD3D<<>>(currOutput, relative); + break; + case Sampling::TransformLinDepthReverse: kComputeLoD3D<<>>(currOutput, relative); + break; + case Sampling::TransformLinDepth: kComputeLoD3D<<>>(currOutput, relative); + break; + default: throw std::invalid_argument("Unsupported coordinate mode."); + } + CUDA_CHECK_RETURN(cudaDeviceSynchronize()); + } + END_SAMPLE("compute LoD kernel"); + + } + + BEGIN_SAMPLE; + { + + } + END_SAMPLE("Free memory"); + LOG("End ComputeLoDKernelLauncher"); +} + +#endif //GOOGLE_CUDA \ No newline at end of file diff --git a/phitest/render/cuda/src/resample_grid.hpp b/phitest/render/cuda/src/resample_grid.hpp new file mode 100644 index 0000000..bcc7de8 --- /dev/null +++ b/phitest/render/cuda/src/resample_grid.hpp @@ -0,0 +1,42 @@ +#pragma once + +#ifndef _INCLUDE_RESAMPLE_GRID +#define _INCLUDE_RESAMPLE_GRID + +//#include "third_party/eigen3/unsupported/Eigen/CXX11/Tensor" +#include "tensorflow/core/framework/tensor_types.h" +#include "tensorflow/core/platform/types.h" +#include "sampling_settings.hpp" + +using GPUDevice = Eigen::GpuDevice; +const bool NORMALIZE_GRADIENTS = true; + +template +struct SampleGridKernel{ + void operator()(const Device& d, + const void* input,const long long int* input_shape, + const float* M, const float* V, const float* P, const float* frustum, int32_t numCameras, + uint8_t* mipAtlas, + const Sampling::CoordinateMode coordinateMode, + const Sampling::SamplerSettings, const bool globalSampling, + void* output, const long long int* output_shape); +}; + +/* +#if GOOGLE_CUDA + +template +struct SampleGridKernel{ + void operator()(const GPUDevice& d, + const void* input,const long long int* input_shape, + const float* M, const float* V, const float* P, const float* frustum, int32_t numCameras, + uint8_t* mipAtlas, + const Sampling::CoordinateMode coordinateMode, + const Sampling::SamplerSettings, const bool globalSampling, + void* output, const long long int* output_shape); +}; + +#endif +*/ + +#endif //_INCLUDE_RESAMPLE_GRID \ No newline at end of file diff --git a/phitest/render/cuda/src/sampling.hpp b/phitest/render/cuda/src/sampling.hpp new file mode 100644 index 0000000..c582ffc --- /dev/null +++ b/phitest/render/cuda/src/sampling.hpp @@ -0,0 +1,1065 @@ +#pragma once + +#ifndef _INCLUDE_SAMPLING +#define _INCLUDE_SAMPLING + +#include +//#include +#include "sampling_settings.hpp" +#include "transformations.hpp" +#include "vectormath_helper.hpp" + +namespace Sampling{ +/* +template +struct CoordinateGenerator{ + + __device__ inline glm::vec4 getSampleCoord3D(glm::ivec3 globalIdx); +}; + +template<> +struct CoordinateGenerator{ + bool inverse; + bool linearDepth; + bool useLoD; + + __device__ inline float _internal_calcLoD(glm::vec3 pos, glm::vec3 dx, glm::vec3 dy, glm::vec3 dz){ + const float px = glm::length(glm::vec3(dx.x, dy.x, dz.x)); + const float py = glm::length(glm::vec3(dx.y, dy.y, dz.y)); + const float pz = glm::length(glm::vec3(dx.z, dy.z, dz.z)); + return log2f(max(px, max(py, pz))); + } + + __device__ inline glm::vec3 _internal_getPos3D(glm::ivec3 globalIdx){ + if(inverse){ + if(linearDepth) return OStoIDXlinearDepth(glm::vec4(globalIdx, 1.f), c_dimensions.input); + else return OStoIDX(glm::vec4(globalIdx, 1.f), c_dimensions.input); + }else{ + if(linearDepth) return IDXtoOSlinearDepth(globalIdx, c_dimensions.output_inv); + else return IDXtoOS(globalIdx, c_dimensions.output_inv); + } + + } + + __device__ inline glm::vec4 getSampleCoord3D(glm::ivec3 globalIdx){ + glm::vec4 pos = glm::vec4(_internal_getPos3D(globalIdx), 0.f); + if(useLoD){ + const glm::vec3 d_x = glm::yxz(pos) - _internal_getPos3D(globalIdx+glm::ivec3(1,0,0)); + const glm::vec3 d_y = glm::yxz(pos) - _internal_getPos3D(globalIdx+glm::ivec3(0,1,0)); + const glm::vec3 d_z = glm::yxz(pos) - _internal_getPos3D(globalIdx+glm::ivec3(0,0,1)); + pos.w = _internal_calcLoD(pos, d_x, d_y, d_z); + } + return pos; + } +}; + +template<> +struct CoordinateGenerator{ + const float4* d_lut; //x,y,z,lod; device pointer + bool relativePosition; + + __device__ inline glm::vec4 getSampleCoord3D(glm::ivec3 globalIdx){ + const float4 pos = vectorIO::readVectorType3D(globalIdx, c_dimensions.output, d_lut); + glm::vec4 samplingPos = glm::vec4(globalIdx, 0.f)*static_cast(relativePosition) + vectorIO::toVector(pos); + return samplingPos; + } +}; +*/ + +// --- Sampling position generation --- +//wrapper for transformations +//LoD computation +//input: index coordinats: float grids indices with +0.5 offset applied (handeled by sampling kernel) + +template +__device__ inline glm::vec4 calcLoD(const glm::vec3 idxCoords); + +template<> +__device__ inline glm::vec4 calcLoD(const glm::vec3 idxCoords){ + const glm::vec3 d_x = (glm::xyz(IDXtoOS(idxCoords + glm::vec3(-1,0,0), c_dimensions.output_inv)) - \ + glm::xyz(IDXtoOS(idxCoords + glm::vec3(1,0,0), c_dimensions.output_inv))) *0.5f; + const glm::vec3 d_y = (glm::xyz(IDXtoOS(idxCoords + glm::vec3(0,-1,0), c_dimensions.output_inv)) - \ + glm::xyz(IDXtoOS(idxCoords + glm::vec3(0,1,0), c_dimensions.output_inv))) *0.5f; + const glm::vec3 d_z = (glm::xyz(IDXtoOS(idxCoords + glm::vec3(0,0,-1), c_dimensions.output_inv)) - \ + glm::xyz(IDXtoOS(idxCoords + glm::vec3(0,0,1), c_dimensions.output_inv))) *0.5f; + + const float px = glm::length(glm::vec3(d_x.x, d_y.x, d_z.x)); + const float py = glm::length(glm::vec3(d_x.y, d_y.y, d_z.y)); + const float pz = glm::length(glm::vec3(d_x.z, d_y.z, d_z.z)); + const float d = max(px, max(py, pz)); + const float lod = max(0.f, log2f(d)); + + //return cell size (x,y,z) and LoD (w) + return glm::vec4(glm::length(d_x), glm::length(d_y), glm::length(d_z), lod); +} +template<> +__device__ inline glm::vec4 calcLoD(const glm::vec3 idxCoords){ + const glm::vec3 d_x = (OStoIDX(glm::vec4(idxCoords + glm::vec3(-1,0,0), 1.f), c_dimensions.input) - \ + OStoIDX(glm::vec4(idxCoords + glm::vec3(1,0,0), 1.f), c_dimensions.input)) *0.5f; + const glm::vec3 d_y = (OStoIDX(glm::vec4(idxCoords + glm::vec3(0,-1,0), 1.f), c_dimensions.input) - \ + OStoIDX(glm::vec4(idxCoords + glm::vec3(0,1,0), 1.f), c_dimensions.input)) *0.5f; + const glm::vec3 d_z = (OStoIDX(glm::vec4(idxCoords + glm::vec3(0,0,-1), 1.f), c_dimensions.input) - \ + OStoIDX(glm::vec4(idxCoords + glm::vec3(0,0,1), 1.f), c_dimensions.input)) *0.5f; + + const float px = glm::length(glm::vec3(d_x.x, d_y.x, d_z.x)); + const float py = glm::length(glm::vec3(d_x.y, d_y.y, d_z.y)); + const float pz = glm::length(glm::vec3(d_x.z, d_y.z, d_z.z)); + const float d = max(px, max(py, pz)); + const float lod = max(0.f, log2f(d)); + + //return cell size (x,y,z) and LoD (w) + return glm::vec4(glm::length(d_x), glm::length(d_y), glm::length(d_z), lod); +} +template<> +__device__ inline glm::vec4 calcLoD(const glm::vec3 idxCoords){ + const glm::vec3 d_x = (glm::xyz(IDXtoOSlinearDepth(idxCoords + glm::vec3(-1,0,0), c_dimensions.output_inv)) - \ + glm::xyz(IDXtoOSlinearDepth(idxCoords + glm::vec3(1,0,0), c_dimensions.output_inv))) *0.5f; + const glm::vec3 d_y = (glm::xyz(IDXtoOSlinearDepth(idxCoords + glm::vec3(0,-1,0), c_dimensions.output_inv)) - \ + glm::xyz(IDXtoOSlinearDepth(idxCoords + glm::vec3(0,1,0), c_dimensions.output_inv))) *0.5f; + const glm::vec3 d_z = (glm::xyz(IDXtoOSlinearDepth(idxCoords + glm::vec3(0,0,-1), c_dimensions.output_inv)) - \ + glm::xyz(IDXtoOSlinearDepth(idxCoords + glm::vec3(0,0,1), c_dimensions.output_inv))) *0.5f; + + const float px = glm::length(glm::vec3(d_x.x, d_y.x, d_z.x)); + const float py = glm::length(glm::vec3(d_x.y, d_y.y, d_z.y)); + const float pz = glm::length(glm::vec3(d_x.z, d_y.z, d_z.z)); + const float d = max(px, max(py, pz)); + const float lod = max(0.f, log2f(d)); + + //return cell size (x,y,z) and LoD (w) + return glm::vec4(glm::length(d_x), glm::length(d_y), glm::length(d_z), lod); +} +template<> +__device__ inline glm::vec4 calcLoD(const glm::vec3 idxCoords){ + const glm::vec3 d_x = (OStoIDXlinearDepth(glm::vec4(idxCoords + glm::vec3(-1,0,0), 1.f), c_dimensions.input) - \ + OStoIDXlinearDepth(glm::vec4(idxCoords + glm::vec3(1,0,0), 1.f), c_dimensions.input)) *0.5f; + const glm::vec3 d_y = (OStoIDXlinearDepth(glm::vec4(idxCoords + glm::vec3(0,-1,0), 1.f), c_dimensions.input) - \ + OStoIDXlinearDepth(glm::vec4(idxCoords + glm::vec3(0,1,0), 1.f), c_dimensions.input)) *0.5f; + const glm::vec3 d_z = (OStoIDXlinearDepth(glm::vec4(idxCoords + glm::vec3(0,0,-1), 1.f), c_dimensions.input) - \ + OStoIDXlinearDepth(glm::vec4(idxCoords + glm::vec3(0,0,1), 1.f), c_dimensions.input)) *0.5f; + + const float px = glm::length(glm::vec3(d_x.x, d_y.x, d_z.x)); + const float py = glm::length(glm::vec3(d_x.y, d_y.y, d_z.y)); + const float pz = glm::length(glm::vec3(d_x.z, d_y.z, d_z.z)); + const float d = max(px, max(py, pz)); + const float lod = max(0.f, log2f(d)); + + //return cell size (x,y,z) and LoD (w) + return glm::vec4(glm::length(d_x), glm::length(d_y), glm::length(d_z), lod); +} + + +template +__device__ inline glm::vec4 samplePos3D(const glm::vec3 idxCoords); +// Sampling position for forward transformation +template<> +__device__ inline glm::vec4 samplePos3D(const glm::vec3 idxCoords){ + const glm::vec3 posOS = IDXtoOS(idxCoords, c_dimensions.output_inv); + return glm::vec4(posOS, 0.f); +} +template<> +__device__ inline glm::vec4 samplePos3D(const glm::vec3 idxCoords){ + const glm::vec3 posOS = IDXtoOS(idxCoords, c_dimensions.output_inv); + const glm::vec3 d_x = posOS - glm::xyz(IDXtoOS(idxCoords+glm::vec3(1,0,0), c_dimensions.output_inv)); + const glm::vec3 d_y = posOS - glm::xyz(IDXtoOS(idxCoords+glm::vec3(0,1,0), c_dimensions.output_inv)); + const glm::vec3 d_z = posOS - glm::xyz(IDXtoOS(idxCoords+glm::vec3(0,0,1), c_dimensions.output_inv)); + const float px = glm::length(glm::vec3(d_x.x, d_y.x, d_z.x)); + const float py = glm::length(glm::vec3(d_x.y, d_y.y, d_z.y)); + const float pz = glm::length(glm::vec3(d_x.z, d_y.z, d_z.z)); + const float d = max(px, max(py, pz)); + const float lod = max(0.f, log2f(d)); + return glm::vec4(posOS, lod); +} +// Sampling position for backward transformation +template<> +__device__ inline glm::vec4 samplePos3D(const glm::vec3 idxCoords){ + const glm::vec3 posIDX = OStoIDX(glm::vec4(idxCoords, 1.f), c_dimensions.input); + return glm::vec4(posIDX, 0.f); +} +template<> +__device__ inline glm::vec4 samplePos3D(const glm::vec3 idxCoords){ + const glm::vec3 posIDX = OStoIDX(glm::vec4(idxCoords, 1.f), c_dimensions.input); + const glm::vec3 d_x = posIDX - OStoIDX(glm::vec4(idxCoords+glm::vec3(1,0,0), 1.f), c_dimensions.input); + const glm::vec3 d_y = posIDX - OStoIDX(glm::vec4(idxCoords+glm::vec3(0,1,0), 1.f), c_dimensions.input); + const glm::vec3 d_z = posIDX - OStoIDX(glm::vec4(idxCoords+glm::vec3(0,0,1), 1.f), c_dimensions.input); + const float px = glm::length(d_x); + const float py = glm::length(d_y); + const float pz = glm::length(d_z); + const float d = max(px, max(py, pz)); + const float lod = max(0.f, log2f(d)); + return glm::vec4(posIDX, lod); +} +// Sampling position for forward transformation with linearized depth +template<> +__device__ inline glm::vec4 samplePos3D(const glm::vec3 idxCoords){ + const glm::vec3 posOS = IDXtoOSlinearDepth(idxCoords, c_dimensions.output_inv); + return glm::vec4(posOS, 0.f); +} +template<> +__device__ inline glm::vec4 samplePos3D(const glm::vec3 idxCoords){ + const glm::vec3 posOS = IDXtoOSlinearDepth(idxCoords, c_dimensions.output_inv); + const glm::vec3 d_x = posOS - glm::xyz(IDXtoOSlinearDepth(idxCoords+glm::vec3(1,0,0), c_dimensions.output_inv)); + const glm::vec3 d_y = posOS - glm::xyz(IDXtoOSlinearDepth(idxCoords+glm::vec3(0,1,0), c_dimensions.output_inv)); + const glm::vec3 d_z = posOS - glm::xyz(IDXtoOSlinearDepth(idxCoords+glm::vec3(0,0,1), c_dimensions.output_inv)); + const float px = glm::length(glm::vec3(d_x.x, d_y.x, d_z.x)); + const float py = glm::length(glm::vec3(d_x.y, d_y.y, d_z.y)); + const float pz = glm::length(glm::vec3(d_x.z, d_y.z, d_z.z)); + const float d = max(px, max(py, pz)); + const float lod = max(0.f, log2f(d)); + return glm::vec4(posOS, lod); +} +// Sampling position for backward transformation with linearized depth +template<> +__device__ inline glm::vec4 samplePos3D(const glm::vec3 idxCoords){ + const glm::vec3 posIDX = OStoIDXlinearDepth(glm::vec4(idxCoords, 1.f), c_dimensions.input); + return glm::vec4(posIDX, 0.f); +} +template<> +__device__ inline glm::vec4 samplePos3D(const glm::vec3 idxCoords){ + const glm::vec3 posIDX = OStoIDXlinearDepth(glm::vec4(idxCoords, 1.f), c_dimensions.input); + const glm::vec3 d_x = posIDX - OStoIDXlinearDepth(glm::vec4(idxCoords+glm::vec3(1,0,0), 1.f), c_dimensions.input); + const glm::vec3 d_y = posIDX - OStoIDXlinearDepth(glm::vec4(idxCoords+glm::vec3(0,1,0), 1.f), c_dimensions.input); + const glm::vec3 d_z = posIDX - OStoIDXlinearDepth(glm::vec4(idxCoords+glm::vec3(0,0,1), 1.f), c_dimensions.input); + const float px = glm::length(d_x);//glm::vec3(d_x.x, d_y.x, d_z.x)); + const float py = glm::length(d_y);//glm::vec3(d_x.y, d_y.y, d_z.y)); + const float pz = glm::length(d_z);//glm::vec3(d_x.z, d_y.z, d_z.z)); + const float d = max(px, max(py, pz)); + const float lod = max(0.f, log2f(d)); + return glm::vec4(posIDX, lod); +} + +//--- Sampling --- +__device__ inline glm::vec3 trilinearWeightsRegular(const glm::vec3 position){ + return glm::fract(position); +} + +/* +//recursive interpolation template +template +//DIM: dimension of grid/interpolation, VF: vector type for position matching DIM, VI: vector type for dimensions +__device__ inline T readInterpolated(const VF weights, const VI ceil, const VI floor, const T* buf, const VI buf_dims){ + +}template +//DIM: dimension of grid/interpolation, VF: vector type for position matching DIM, VI: vector type for dimensions +__device__ inline T readInterpolated(const VF weights, const VI ceil, const VI floor, const T* buf, const VI buf_dims){ +} +*/ + +__device__ inline float getQuadraticBSplineWeight(const float distance){ + if(distance<0.5f){ + return 0.75f - distance*distance; + }else if(distance<1.5f){ + const float tmp = 1.5 - distance; + return 0.5*tmp*tmp; + }else{ + return 0.0f; + } +} +/* +* Cell centers at +0.0 offset, spacing is uniformly 1.0. +* interpolate per-axis using quadratic B-splines +* https://cg.informatik.uni-freiburg.de/intern/seminar/animation%20-%20MPM%20survey%20-%202016.pdf page 33, equation 123 +* weight = +* 3/4 - |x|^2 if 0<=x<1/2 +* 1/2(3/2 - |x|)^2 if 1/2<=x<3/2 +* 0 else +* where x is the distance between sampling location and interpolant +* this results in a 3x3x3 stencil +*/ +template +__device__ inline T read3DQuadratic(const glm::vec3 position, const T* buf, const glm::ivec3 buf_dims){ + + glm::ivec3 centerIdx = glm::ivec3(glm::floor(position + 0.5f)); + glm::ivec3 prevIdx = centerIdx - 1; + glm::ivec3 nextIdx = centerIdx + 1; + + const glm::vec3 prevDist = glm::abs(position - glm::vec3(prevIdx)); + const glm::vec3 prevWeights = glm::vec3(getQuadraticBSplineWeight(prevDist.x), getQuadraticBSplineWeight(prevDist.y), getQuadraticBSplineWeight(prevDist.z)); + + const glm::vec3 centerDist = glm::abs(position - glm::vec3(centerIdx)); + const glm::vec3 centerWeights = glm::vec3(getQuadraticBSplineWeight(centerDist.x), getQuadraticBSplineWeight(centerDist.y), getQuadraticBSplineWeight(centerDist.z)); + + const glm::vec3 nextDist = glm::abs(position - glm::vec3(nextIdx)); + const glm::vec3 nextWeights = glm::vec3(getQuadraticBSplineWeight(nextDist.x), getQuadraticBSplineWeight(nextDist.y), getQuadraticBSplineWeight(nextDist.z)); + + if (BM == SamplerSettings::BORDER){// const 0 outside domain + //if(!isInDimensions(position+0.5f, buf_dims) || !isNonNegative(position+0.5f)){ + return vmath::make_cudaFloat(0.f); + //} + }else{//here the indices are always valid, so no special handling needed + if(BM == SamplerSettings::CLAMP){ + prevIdx = glm::clamp(prevIdx, glm::ivec3(0), buf_dims -1); + centerIdx = glm::clamp(centerIdx, glm::ivec3(0), buf_dims -1); + nextIdx = glm::clamp(nextIdx, glm::ivec3(0), buf_dims -1); + } + else if (BM == SamplerSettings::WRAP){//periodic + prevIdx = vmath::positivemod(prevIdx, buf_dims); + centerIdx = vmath::positivemod(centerIdx, buf_dims); + nextIdx = vmath::positivemod(nextIdx, buf_dims); + } + + //read and interpolate along x + const T v00 = + prevWeights.x * vectorIO::readVectorType3D(prevIdx.x, prevIdx.y, prevIdx.z, buf_dims, buf) + + centerWeights.x * vectorIO::readVectorType3D(centerIdx.x, prevIdx.y, prevIdx.z, buf_dims, buf) + + nextWeights.x * vectorIO::readVectorType3D(nextIdx.x, prevIdx.y, prevIdx.z, buf_dims, buf); + const T v01 = + prevWeights.x * vectorIO::readVectorType3D(prevIdx.x, centerIdx.y, prevIdx.z, buf_dims, buf) + + centerWeights.x * vectorIO::readVectorType3D(centerIdx.x, centerIdx.y, prevIdx.z, buf_dims, buf) + + nextWeights.x * vectorIO::readVectorType3D(nextIdx.x, centerIdx.y, prevIdx.z, buf_dims, buf); + const T v02 = + prevWeights.x * vectorIO::readVectorType3D(prevIdx.x, nextIdx.y, prevIdx.z, buf_dims, buf) + + centerWeights.x * vectorIO::readVectorType3D(centerIdx.x, nextIdx.y, prevIdx.z, buf_dims, buf) + + nextWeights.x * vectorIO::readVectorType3D(nextIdx.x, nextIdx.y, prevIdx.z, buf_dims, buf); + + //interpolate along y + const T v0 = + prevWeights.y * v00 + + centerWeights.y * v01 + + nextWeights.y * v02; + + //read and interpolate along x + const T v10 = + prevWeights.x * vectorIO::readVectorType3D(prevIdx.x, prevIdx.y, centerIdx.z, buf_dims, buf) + + centerWeights.x * vectorIO::readVectorType3D(centerIdx.x, prevIdx.y, centerIdx.z, buf_dims, buf) + + nextWeights.x * vectorIO::readVectorType3D(nextIdx.x, prevIdx.y, centerIdx.z, buf_dims, buf); + const T v11 = + prevWeights.x * vectorIO::readVectorType3D(prevIdx.x, centerIdx.y, centerIdx.z, buf_dims, buf) + + centerWeights.x * vectorIO::readVectorType3D(centerIdx.x, centerIdx.y, centerIdx.z, buf_dims, buf) + + nextWeights.x * vectorIO::readVectorType3D(nextIdx.x, centerIdx.y, centerIdx.z, buf_dims, buf); + const T v12 = + prevWeights.x * vectorIO::readVectorType3D(prevIdx.x, nextIdx.y, centerIdx.z, buf_dims, buf) + + centerWeights.x * vectorIO::readVectorType3D(centerIdx.x, nextIdx.y, centerIdx.z, buf_dims, buf) + + nextWeights.x * vectorIO::readVectorType3D(nextIdx.x, nextIdx.y, centerIdx.z, buf_dims, buf); + + //interpolate along y + const T v1 = + prevWeights.y * v10 + + centerWeights.y * v11 + + nextWeights.y * v12; + + //read and interpolate along x + const T v20 = + prevWeights.x * vectorIO::readVectorType3D(prevIdx.x, prevIdx.y, nextIdx.z, buf_dims, buf) + + centerWeights.x * vectorIO::readVectorType3D(centerIdx.x, prevIdx.y, nextIdx.z, buf_dims, buf) + + nextWeights.x * vectorIO::readVectorType3D(nextIdx.x, prevIdx.y, nextIdx.z, buf_dims, buf); + const T v21 = + prevWeights.x * vectorIO::readVectorType3D(prevIdx.x, centerIdx.y, nextIdx.z, buf_dims, buf) + + centerWeights.x * vectorIO::readVectorType3D(centerIdx.x, centerIdx.y, nextIdx.z, buf_dims, buf) + + nextWeights.x * vectorIO::readVectorType3D(nextIdx.x, centerIdx.y, nextIdx.z, buf_dims, buf); + const T v22 = + prevWeights.x * vectorIO::readVectorType3D(prevIdx.x, nextIdx.y, nextIdx.z, buf_dims, buf) + + centerWeights.x * vectorIO::readVectorType3D(centerIdx.x, nextIdx.y, nextIdx.z, buf_dims, buf) + + nextWeights.x * vectorIO::readVectorType3D(nextIdx.x, nextIdx.y, nextIdx.z, buf_dims, buf); + + //interpolate along y + const T v2 = + prevWeights.y * v20 + + centerWeights.y * v21 + + nextWeights.y * v22; + + //interpolate along z + return + prevWeights.z * v0 + + centerWeights.z * v1 + + nextWeights.z * v2; + } +} + +/* +* Cell centers at +0.0 offset, spacing is uniformly 1.0. The fractional part of the sampling coordinate is the interpolation weight. +*/ +template +__device__ inline T read3DInterpolated(const glm::vec3 position, const T* buf, const glm::ivec3 buf_dims){ + + //weights for ceil, floor weights are (1-weights) + const glm::vec3 weights = trilinearWeightsRegular(position); + + //calculate corner indices + glm::ivec3 ceilIdx = glm::ivec3(glm::ceil(position)); + glm::ivec3 floorIdx = glm::ivec3(glm::floor(position)); + + if (BM == SamplerSettings::BORDER){// const 0 outside domain + if(!isInDimensions(position+0.5f, buf_dims) || !isNonNegative(position+0.5f)){ + return vmath::make_cudaFloat(0.f); + } + glm::ivec3 ceilValid = (-1 < ceilIdx)*( ceilIdx < buf_dims); + glm::ivec3 floorValid = (-1 < floorIdx)*(floorIdx < buf_dims); + T data = {0}; + //read and interpolate along x + const T v00 = vmath::lerp( + (floorValid.x*floorValid.y*floorValid.z)>0.f + ? vectorIO::readVectorType3D(floorIdx.x, floorIdx.y, floorIdx.z, buf_dims, buf) + : data, + ( ceilValid.x*floorValid.y*floorValid.z)>0.f + ? vectorIO::readVectorType3D( ceilIdx.x, floorIdx.y, floorIdx.z, buf_dims, buf) + : data, + weights.x); + const T v01 = vmath::lerp( + (floorValid.x* ceilValid.y*floorValid.z)>0.f + ? vectorIO::readVectorType3D(floorIdx.x, ceilIdx.y, floorIdx.z, buf_dims, buf) + : data, + ( ceilValid.x* ceilValid.y*floorValid.z)>0.f + ? vectorIO::readVectorType3D( ceilIdx.x, ceilIdx.y, floorIdx.z, buf_dims, buf) + : data, + weights.x); + //interpolate along y + const T v0 = vmath::lerp(v00, v01, weights.y); + + + const T v10 = vmath::lerp( + (floorValid.x*floorValid.y* ceilValid.z)>0.f + ? vectorIO::readVectorType3D(floorIdx.x, floorIdx.y, ceilIdx.z, buf_dims, buf) + : data, + ( ceilValid.x*floorValid.y* ceilValid.z)>0.f + ? vectorIO::readVectorType3D( ceilIdx.x, floorIdx.y, ceilIdx.z, buf_dims, buf) + : data, + weights.x); + const T v11 = vmath::lerp( + (floorValid.x* ceilValid.y* ceilValid.z)>0.f + ? vectorIO::readVectorType3D(floorIdx.x, ceilIdx.y, ceilIdx.z, buf_dims, buf) + : data, + ( ceilValid.x* ceilValid.y* ceilValid.z)>0.f + ? vectorIO::readVectorType3D( ceilIdx.x, ceilIdx.y, ceilIdx.z, buf_dims, buf) + : data, + weights.x); + //interpolate along y + const T v1 = vmath::lerp(v10, v11, weights.y); + + + //interpolate along z + return vmath::lerp(v0, v1, weights.z); //(1-weights.z)*yValue.x + weights.z*yValue.y; + + }else{//here the indices are always valid, so no special handling needed + if(BM == SamplerSettings::CLAMP){ + ceilIdx = glm::clamp(ceilIdx, glm::ivec3(0), buf_dims -1); + floorIdx = glm::clamp(floorIdx, glm::ivec3(0), buf_dims -1); + } + else if (BM == SamplerSettings::WRAP){//periodic + ceilIdx = vmath::positivemod(ceilIdx, buf_dims); + floorIdx = vmath::positivemod(floorIdx, buf_dims); + } + //read and interpolate along x + const T v00 = vmath::lerp( vectorIO::readVectorType3D(floorIdx.x, floorIdx.y, floorIdx.z, buf_dims, buf), + vectorIO::readVectorType3D( ceilIdx.x, floorIdx.y, floorIdx.z, buf_dims, buf), + weights.x); + const T v01 = vmath::lerp( vectorIO::readVectorType3D(floorIdx.x, ceilIdx.y, floorIdx.z, buf_dims, buf), + vectorIO::readVectorType3D( ceilIdx.x, ceilIdx.y, floorIdx.z, buf_dims, buf), + weights.x); + //interpolate along y + const T v0 = vmath::lerp(v00, v01, weights.y); + + + const T v10 = vmath::lerp( vectorIO::readVectorType3D(floorIdx.x, floorIdx.y, ceilIdx.z, buf_dims, buf), + vectorIO::readVectorType3D( ceilIdx.x, floorIdx.y, ceilIdx.z, buf_dims, buf), + weights.x); + const T v11 = vmath::lerp( vectorIO::readVectorType3D(floorIdx.x, ceilIdx.y, ceilIdx.z, buf_dims, buf), + vectorIO::readVectorType3D( ceilIdx.x, ceilIdx.y, ceilIdx.z, buf_dims, buf), + weights.x); + //interpolate along y + const T v1 = vmath::lerp(v10, v11, weights.y); + + + //interpolate along z + return vmath::lerp(v0, v1, weights.z); //(1-weights.z)*yValue.x + weights.z*yValue.y; + } +} +template +struct DataGrad3D{ + T dx,dz,dy; +}; +template +__device__ inline DataGrad3D read3DGrad(const glm::vec3 position, const T* buf, const glm::ivec3 buf_dims){ + + //weights for ceil, floor weights are (1-weights) + const glm::vec3 weights = trilinearWeightsRegular(position); + + //calculate corner indices + glm::ivec3 ceilIdx = glm::ivec3(glm::ceil(position)); + glm::ivec3 floorIdx = glm::ivec3(glm::floor(position)); + + if (BM == SamplerSettings::BORDER){// const 0 outside domain + if(!isInDimensions(position+0.5f, buf_dims) || !isNonNegative(position+0.5f)){ + DataGrad3D zero = {0}; + return zero; + } + glm::ivec3 ceilValid = (-1 < ceilIdx)*( ceilIdx < buf_dims); + glm::ivec3 floorValid = (-1 < floorIdx)*(floorIdx < buf_dims); + T data = {0}; + //read + const T fxfyfz = (floorValid.x*floorValid.y*floorValid.z) + ? vectorIO::readVectorType3D(floorIdx.x, floorIdx.y, floorIdx.z, buf_dims, buf) + : data; + const T cxfyfz = ( ceilValid.x*floorValid.y*floorValid.z) + ? vectorIO::readVectorType3D( ceilIdx.x, floorIdx.y, floorIdx.z, buf_dims, buf) + : data; + const T fxcyfz = (floorValid.x* ceilValid.y*floorValid.z) + ? vectorIO::readVectorType3D(floorIdx.x, ceilIdx.y, floorIdx.z, buf_dims, buf) + : data; + const T cxcyfz = ( ceilValid.x* ceilValid.y*floorValid.z) + ? vectorIO::readVectorType3D( ceilIdx.x, ceilIdx.y, floorIdx.z, buf_dims, buf) + : data; + const T fxfycz = (floorValid.x*floorValid.y* ceilValid.z) + ? vectorIO::readVectorType3D(floorIdx.x, floorIdx.y, ceilIdx.z, buf_dims, buf) + : data; + const T cxfycz = ( ceilValid.x*floorValid.y* ceilValid.z) + ? vectorIO::readVectorType3D( ceilIdx.x, floorIdx.y, ceilIdx.z, buf_dims, buf) + : data; + const T fxcycz = (floorValid.x* ceilValid.y* ceilValid.z) + ? vectorIO::readVectorType3D(floorIdx.x, ceilIdx.y, ceilIdx.z, buf_dims, buf) + : data; + const T cxcycz = ( ceilValid.x* ceilValid.y* ceilValid.z) + ? vectorIO::readVectorType3D( ceilIdx.x, ceilIdx.y, ceilIdx.z, buf_dims, buf) + : data; + //interpolate differences + DataGrad3D dataGrad; + dataGrad.dx = vmath::lerp( + vmath::lerp((cxfyfz-fxfyfz),(cxcyfz-fxcyfz), weights.y), + vmath::lerp((cxfycz-fxfycz),(cxcycz-fxcycz), weights.y), + weights.z); + dataGrad.dy = vmath::lerp( + vmath::lerp((fxcyfz-fxfyfz),(cxcyfz-cxfyfz), weights.x), + vmath::lerp((fxcycz-fxfycz),(cxcycz-cxfycz), weights.x), + weights.z); + dataGrad.dz = vmath::lerp( + vmath::lerp((fxfycz-fxfyfz),(cxfycz-cxfyfz), weights.x), + vmath::lerp((fxcycz-fxcyfz),(cxcycz-cxcyfz), weights.x), + weights.y); + + return dataGrad; + + }else{//here the indices are always valid, so no special handling needed + if(BM == SamplerSettings::CLAMP){ + ceilIdx = glm::clamp(ceilIdx, glm::ivec3(0), buf_dims -1); + floorIdx = glm::clamp(floorIdx, glm::ivec3(0), buf_dims -1); + } + else if (BM == SamplerSettings::WRAP){//periodic + ceilIdx = vmath::positivemod(ceilIdx, buf_dims); + floorIdx = vmath::positivemod(floorIdx, buf_dims); + } + //read + const T fxfyfz = vectorIO::readVectorType3D(floorIdx.x, floorIdx.y, floorIdx.z, buf_dims, buf); + const T cxfyfz = vectorIO::readVectorType3D( ceilIdx.x, floorIdx.y, floorIdx.z, buf_dims, buf); + const T fxcyfz = vectorIO::readVectorType3D(floorIdx.x, ceilIdx.y, floorIdx.z, buf_dims, buf); + const T cxcyfz = vectorIO::readVectorType3D( ceilIdx.x, ceilIdx.y, floorIdx.z, buf_dims, buf); + const T fxfycz = vectorIO::readVectorType3D(floorIdx.x, floorIdx.y, ceilIdx.z, buf_dims, buf); + const T cxfycz = vectorIO::readVectorType3D( ceilIdx.x, floorIdx.y, ceilIdx.z, buf_dims, buf); + const T fxcycz = vectorIO::readVectorType3D(floorIdx.x, ceilIdx.y, ceilIdx.z, buf_dims, buf); + const T cxcycz = vectorIO::readVectorType3D( ceilIdx.x, ceilIdx.y, ceilIdx.z, buf_dims, buf); + //interpolate differences + DataGrad3D dataGrad; + dataGrad.dx = vmath::lerp( + vmath::lerp((cxfyfz-fxfyfz),(cxcyfz-fxcyfz), weights.y), + vmath::lerp((cxfycz-fxfycz),(cxcycz-fxcycz), weights.y), + weights.z); + dataGrad.dy = vmath::lerp( + vmath::lerp((fxcyfz-fxfyfz),(cxcyfz-cxfyfz), weights.x), + vmath::lerp((fxcycz-fxfycz),(cxcycz-cxfycz), weights.x), + weights.z); + dataGrad.dz = vmath::lerp( + vmath::lerp((fxfycz-fxfyfz),(cxfycz-cxfyfz), weights.x), + vmath::lerp((fxcycz-fxcyfz),(cxcycz-cxcyfz), weights.x), + weights.y); + + return dataGrad; + } +} +/* +* Cell centers at +0.0 offset, spacing is uniformly 1.0. The fractional part of the sampling coordinate is the interpolation weight. +*/ +template +__device__ inline T read3DNearest(const glm::vec3 position, const T* buf, const glm::ivec3 buf_dims){ + glm::ivec3 idx = glm::ivec3(position +.5f); + if (BM == SamplerSettings::BORDER){ + if(!isInDimensions(idx, buf_dims) || !isNonNegative(idx)){ + return vmath::make_cudaFloat(0.f); + } + } + else if(BM == SamplerSettings::CLAMP){ + idx = glm::clamp(idx, glm::ivec3(0), buf_dims -1); + } + else if (BM == SamplerSettings::WRAP){//periodic + idx = vmath::positivemod(idx, buf_dims); + } + return vectorIO::readVectorType3D(idx, buf_dims, buf); +} +template +__device__ inline T read3DMin(const glm::vec3 position, const T* buf, const glm::ivec3 buf_dims){ + + + //calculate corner indices + glm::ivec3 ceilIdx = glm::ivec3(glm::ceil(position)); + glm::ivec3 floorIdx = glm::ivec3(glm::floor(position)); + + if (BM == SamplerSettings::BORDER){// const 0 outside domain + if(!isInDimensions(position+0.5f, buf_dims) || !isNonNegative(position+0.5f)){ + return vmath::make_cudaFloat(0.f); + } + glm::ivec3 ceilValid = (-1 < ceilIdx)*( ceilIdx < buf_dims); + glm::ivec3 floorValid = (-1 < floorIdx)*(floorIdx < buf_dims); + T data; + if(!(vmath::prod(ceilValid)*vmath::prod(floorValid))){ //any cell invalid/out of bounds + data = vmath::make_cudaFloat(0.f); + }else{ + data = vmath::make_cudaFloat(FLOAT_MAX); + } + #define cmpMin(vx, vy, vz) if((vx##Valid.x* vy##Valid.y* vz##Valid.z)>0.f) data = fminf(data, vectorIO::readVectorType3D(vx##Idx.x, vy##Idx.y, vz##Idx.z, buf_dims, buf)) + //if((floorValid.x*floorValid.y*floorValid.z)>0.f) data = vmath::min(data, vectorIO::readVectorType3D(floorIdx.x, floorIdx.y, floorIdx.z, buf_dims, buf)); + cmpMin(floor, floor, floor); + cmpMin( ceil, floor, floor); + cmpMin(floor, ceil, floor); + cmpMin( ceil, ceil, floor); + + cmpMin(floor, floor, ceil); + cmpMin( ceil, floor, ceil); + cmpMin(floor, ceil, ceil); + cmpMin( ceil, ceil, ceil); + #undef cmpMin + return data; + }else{//here the indices are always valid, so no special handling needed + if(BM == SamplerSettings::CLAMP){ + ceilIdx = glm::clamp(ceilIdx, glm::ivec3(0), buf_dims -1); + floorIdx = glm::clamp(floorIdx, glm::ivec3(0), buf_dims -1); + } + else if (BM == SamplerSettings::WRAP){//periodic + ceilIdx = vmath::positivemod(ceilIdx, buf_dims); + floorIdx = vmath::positivemod(floorIdx, buf_dims); + } + T data = vmath::make_cudaFloat(FLOAT_MAX); + + #define cmpMin(vx, vy, vz) data = fminf(data, vectorIO::readVectorType3D(vx##Idx.x, vy##Idx.y, vz##Idx.z, buf_dims, buf)) + //data = vmath::min(data, vectorIO::readVectorType3D(floorIdx.x, floorIdx.y, floorIdx.z, buf_dims, buf)); + cmpMin(floor, floor, floor); + cmpMin( ceil, floor, floor); + cmpMin(floor, ceil, floor); + cmpMin( ceil, ceil, floor); + + cmpMin(floor, floor, ceil); + cmpMin( ceil, floor, ceil); + cmpMin(floor, ceil, ceil); + cmpMin( ceil, ceil, ceil); + #undef cmpMin + + return data; + } +} +template +__device__ inline T read3DMax(const glm::vec3 position, const T* buf, const glm::ivec3 buf_dims){ + + + //calculate corner indices + glm::ivec3 ceilIdx = glm::ivec3(glm::ceil(position)); + glm::ivec3 floorIdx = glm::ivec3(glm::floor(position)); + + if (BM == SamplerSettings::BORDER){// const 0 outside domain + if(!isInDimensions(position+0.5f, buf_dims) || !isNonNegative(position+0.5f)){ + return vmath::make_cudaFloat(0.f); + } + glm::ivec3 ceilValid = (-1 < ceilIdx)*( ceilIdx < buf_dims); + glm::ivec3 floorValid = (-1 < floorIdx)*(floorIdx < buf_dims); + T data; + if(!(vmath::prod(ceilValid)*vmath::prod(floorValid))){ //any cell invalid/out of bounds + data = vmath::make_cudaFloat(0.f); + }else{ + data = vmath::make_cudaFloat(FLOAT_LOWEST); + } + #define cmpMax(vx, vy, vz) if((vx##Valid.x* vy##Valid.y* vz##Valid.z)>0.f) data = fmaxf(data, vectorIO::readVectorType3D(vx##Idx.x, vy##Idx.y, vz##Idx.z, buf_dims, buf)) + //if((floorValid.x*floorValid.y*floorValid.z)>0.f) data = vmath::max(data, vectorIO::readVectorType3D(floorIdx.x, floorIdx.y, floorIdx.z, buf_dims, buf)); + cmpMax(floor, floor, floor); + cmpMax( ceil, floor, floor); + cmpMax(floor, ceil, floor); + cmpMax( ceil, ceil, floor); + + cmpMax(floor, floor, ceil); + cmpMax( ceil, floor, ceil); + cmpMax(floor, ceil, ceil); + cmpMax( ceil, ceil, ceil); + #undef cmpMax + return data; + }else{//here the indices are always valid, so no special handling needed + if(BM == SamplerSettings::CLAMP){ + ceilIdx = glm::clamp(ceilIdx, glm::ivec3(0), buf_dims -1); + floorIdx = glm::clamp(floorIdx, glm::ivec3(0), buf_dims -1); + } + else if (BM == SamplerSettings::WRAP){//periodic + ceilIdx = vmath::positivemod(ceilIdx, buf_dims); + floorIdx = vmath::positivemod(floorIdx, buf_dims); + } + T data = vmath::make_cudaFloat(FLOAT_LOWEST); + + #define cmpMax(vx, vy, vz) data = fmaxf(data, vectorIO::readVectorType3D(vx##Idx.x, vy##Idx.y, vz##Idx.z, buf_dims, buf)) + //data = vmath::min(data, vectorIO::readVectorType3D(floorIdx.x, floorIdx.y, floorIdx.z, buf_dims, buf)); + cmpMax(floor, floor, floor); + cmpMax( ceil, floor, floor); + cmpMax(floor, ceil, floor); + cmpMax( ceil, ceil, floor); + + cmpMax(floor, floor, ceil); + cmpMax( ceil, floor, ceil); + cmpMax(floor, ceil, ceil); + cmpMax( ceil, ceil, ceil); + #undef cmpMax + + return data; + } +} + + +/* +//recursive interpolation template +template +//DIM: dimension of grid/interpolation, VF: vector type for position matching DIM, VI: vector type for dimensions +__device__ inline T readInterpolatedChannel(const glm::vec3 position, const int32_t channel, const T* buf, const glm::ivec3 buf_dims, const int32_t buf_channel_dim); +*/ +template +__device__ inline T read3DInterpolatedChannel(const glm::vec3 position, const int32_t channel, const T* buf, const glm::ivec3 buf_dims, const int32_t buf_channel_dim){ + //weights for ceil, floor weights are (1-weights) + const glm::vec3 weights = trilinearWeightsRegular(position); + + //calculate corner indices + const glm::ivec3 ceilIdx = glm::ceil(position); + const glm::ivec3 floorIdx = glm::floor(position); + + //read and interpolate along x + //channel_dim*(dims.x*(dims.y*pos.z + pos.y) + pos.x) + channel + size_t idxTmp = buf_dims.x*(buf_dims.y*floorIdx.z + floorIdx.y); + const T v00 = vmath::lerp( buf[buf_channel_dim*(idxTmp + floorIdx.x) + channel], + buf[buf_channel_dim*(idxTmp + ceilIdx.x) + channel], + weights.x); + idxTmp = buf_dims.x*(buf_dims.y*floorIdx.z + ceilIdx.y); + const T v01 = vmath::lerp( buf[buf_channel_dim*(idxTmp + floorIdx.x) + channel], + buf[buf_channel_dim*(idxTmp + ceilIdx.x) + channel], + weights.x); + //interpolate along y + const T v0 = vmath::lerp(v00, v01, weights.y); + + idxTmp = buf_dims.x*(buf_dims.y*floorIdx.z + ceilIdx.y); + const T v10 = vmath::lerp( buf[buf_channel_dim*(idxTmp + floorIdx.x) + channel], + buf[buf_channel_dim*(idxTmp + ceilIdx.x) + channel], + weights.x); + idxTmp = buf_dims.x*(buf_dims.y*ceilIdx.z + ceilIdx.y); + const T v11 = vmath::lerp( buf[buf_channel_dim*(idxTmp + floorIdx.x) + channel], + buf[buf_channel_dim*(idxTmp + ceilIdx.x) + channel], + weights.x); + //interpolate along y + const T v1 = vmath::lerp(v10, v11, weights.y); + + //interpolate along z + return vmath::lerp(v0, v1, weights.z); //(1-weights.z)*yValue.x + weights.z*yValue.y; +} +/* +template +struct InterpolationData3D{ + const T fzfyfx; + const T fzfycx; + const T fzcyfx; + const T fzcycx; + const T czfyfx; + const T czfycx; + const T czcyfx; + const T czcycx; + const glm::vec3 ceilWeights; +} +*/ +//#define ALIGN_CELL_CENTER +// cell centers are at +0.5 offset +//template +template +struct Sampler2{ + SamplerSettings settings; + glm::ivec4 dimensions; + //float cellCenterOffset; + + union{ + const T_DATA UG_PTR *d_input; //device pointer + const T_DATA UG_PTR *const *d_mips; //device pointer + //cudaTextureObject_t texture; + }; + + __device__ constexpr float getLoD(const glm::vec4 position) const { + return glm::clamp(position.w + settings.mipBias, settings.mipClampMin, settings.mipClampMax); + } + + __device__ constexpr glm::vec3 getSamplingPosition(const glm::vec3 position) const { + return position - settings.cellCenterOffset; + } + + __device__ constexpr glm::vec3 getCeilWeights(const glm::vec3 position) const { + return glm::fract(position - settings.cellCenterOffset); + } + + __device__ inline int32_t getMipLevelFloor(const float lod) const { + return (settings.mipMode==SamplerSettings::MIPMODE_NONE)? + 0 : + min(static_cast(lod+.5f*(settings.mipMode==SamplerSettings::MIPMODE_NEAREST)), settings.mipLevel); + } + //should only be called if settings.mipMode==SamplerSettings::MipMode::LINEAR + __device__ inline int32_t getMipLevelCeil(const float lod) const { + return min(static_cast(ceil(lod)), settings.mipLevel); + } + + // --- Sampling --- + + __device__ inline T_DATA _internal_fetch3D(const glm::vec3 position, const glm::ivec3 buf_dimensions, const T_DATA *buf) const { + T_DATA data = {0}; + // undo center offset to +0.0 as needed by the interpolation + //position -= settings.cellCenterOffset; + if(settings.filterMode==SamplerSettings::FILTERMODE_QUADRATIC){ + if(settings.boundaryMode==SamplerSettings::BORDER){ + data = read3DQuadratic(position - settings.cellCenterOffset, buf, buf_dimensions); + }else if(settings.boundaryMode==SamplerSettings::CLAMP){ + data = read3DQuadratic(position - settings.cellCenterOffset, buf, buf_dimensions); + } + }else if(settings.filterMode==SamplerSettings::FILTERMODE_LINEAR){ + if(settings.boundaryMode==SamplerSettings::BORDER){ + data = read3DInterpolated(position - settings.cellCenterOffset, buf, buf_dimensions); + }else if(settings.boundaryMode==SamplerSettings::CLAMP){ + data = read3DInterpolated(position - settings.cellCenterOffset, buf, buf_dimensions); + } + } + else{ //NEAREST + if(settings.boundaryMode==SamplerSettings::BORDER){ + data = read3DNearest(position - settings.cellCenterOffset, buf, buf_dimensions); + }else if(settings.boundaryMode==SamplerSettings::CLAMP){ + data = read3DNearest(position - settings.cellCenterOffset, buf, buf_dimensions); + } + } + return data; + } + __device__ inline T_DATA read3D(const glm::vec3 position) const { + return _internal_fetch3D(position, dimensions, d_input); + } + __device__ inline T_DATA read3DLevel(const glm::vec3 position, const int32_t mipLevel) const { + const glm::ivec3 mipDims = dimensions>>mipLevel; + const glm::vec3 mipPos = position/static_cast(1<(texture, position.x+.5f, position.y+.5f, position.z+.5f, position.w); + }else{ + return tex3D(texture, position.x+.5f, position.y+.5f, position.z+.5f); + } + }else */ + if(settings.mipMode==SamplerSettings::MIPMODE_NONE){ + return read3D(position); + }else{ + const float lod = getLoD(position); + //if NONE: input, if NEAREST: mip[int(mip+0.5)], if LINEAR: mip[int(mip)] + const int32_t mipLevelFloor = getMipLevelFloor(lod); + T_DATA data = read3DLevel(position, mipLevelFloor); + + if(settings.mipMode==SamplerSettings::MIPMODE_LINEAR){ + const int32_t mipLevelCeil = getMipLevelCeil(lod); + if(mipLevelCeil>mipLevelFloor){ + //const T_DATA *mipCeil = mips[mipLevelCeil]; + data = vmath::lerp(data, read3DLevel(position, mipLevelCeil), glm::fract(lod)); + } + } + return data; + } + } + __device__ inline T_DATA sample3Dnormalized(const glm::vec4 normalizedPosition) const { + glm::vec4 position = normalizedPosition; + position.x *= dimensions.x; + position.y *= dimensions.y; + position.z *= dimensions.z; + return sample3D(position); + } + + // --- Data Gradients --- + + __device__ inline DataGrad3D _internal_fetchGrad3D(const glm::vec3 position, const glm::ivec3 buf_dimensions, const T_DATA *buf) const { + DataGrad3D data = {0}; + // undo center offset to +0.0 as needed by the interpolation + //position -= settings.cellCenterOffset; + if(settings.filterMode==SamplerSettings::FILTERMODE_LINEAR){ + if(settings.boundaryMode==SamplerSettings::BORDER){ + data = read3DGrad(position - settings.cellCenterOffset, buf, buf_dimensions); + }else if(settings.boundaryMode==SamplerSettings::CLAMP){ + data = read3DGrad(position - settings.cellCenterOffset, buf, buf_dimensions); + } + } + else{ //NEAREST + // return 0 as data gradient for NN sampling + } + return data; + } + __device__ inline DataGrad3D readGrad3D(const glm::vec3 position) const { + return _internal_fetchGrad3D(position, dimensions, d_input); + } + __device__ inline DataGrad3D readGrad3DLevel(const glm::vec3 position, int32_t mipLevel) const { + const glm::ivec3 mipDims = dimensions>>mipLevel; + const glm::vec3 mipPos = position/static_cast(1< sampleGrad3D(const glm::vec4 position) const { + if(settings.mipMode==SamplerSettings::MIPMODE_NONE){ + return readGrad3D(position); + }else{ + const float lod = getLoD(position); + //if NONE: input, if NEAREST: mip[int(mip+0.5)], if LINEAR: mip[int(mip)] + const int32_t mipLevelFloor = getMipLevelFloor(lod); + DataGrad3D data = readGrad3DLevel(position, mipLevelFloor); + + if(settings.mipMode==SamplerSettings::MIPMODE_LINEAR){ + const int32_t mipLevelCeil = getMipLevelCeil(lod); + if(mipLevelCeil>mipLevelFloor){ + //const T_DATA *mipCeil = mips[mipLevelCeil]; + float lodWeight = glm::fract(lod); + DataGrad3D data2 = readGrad3DLevel(position, mipLevelCeil); + data.dx = vmath::lerp(data.dx,data2.dx, lodWeight); + data.dy = vmath::lerp(data.dy,data2.dy, lodWeight); + data.dz = vmath::lerp(data.dz,data2.dz, lodWeight); + } + } + return data; + } + } +}; + +/* +* Sampling templates +*/ + +template +struct Grid3D{ + glm::ivec4 dimensions; //x,y,z,channel + glm::vec3 dimensionsInverse; + int32_t mipLevel; + union{ + T_DATA UG_PTR *d_data; //device pointer + T_DATA UG_PTR *const *d_mips; //device pointer + }; +}; + +__device__ inline float getLoD(const SamplerSettings settings, const glm::vec4 position){ + return glm::clamp(position.w + settings.mipBias, settings.mipClampMin, settings.mipClampMax); +} + +template +__device__ inline int32_t getMipLevelFloor(const float lod, const int32_t maxLod){ + return (MM==SamplerSettings::MIPMODE_NONE)? + 0 : + min(static_cast(lod+.5f*(MM==SamplerSettings::MIPMODE_NEAREST)), maxLod); +} +//should only be called if settings.mipMode==SamplerSettings::MipMode::LINEAR +__device__ inline int32_t getMipLevelCeil(const float lod, const int32_t maxLod){ + return min(static_cast(ceil(lod)), maxLod); +} + +// --- Sampling --- + +template +__device__ inline T_DATA _internal_fetch3D(const SamplerSettings settings, const glm::vec3 position, const glm::ivec3 buf_dimensions, const T_DATA *buf){ + T_DATA data = {0}; + // undo center offset to +0.0 as needed by the interpolation + //position -= settings.cellCenterOffset; + if(FM==SamplerSettings::FILTERMODE_QUADRATIC){ + data = read3DQuadratic(position - settings.cellCenterOffset, buf, buf_dimensions); + } + else if(FM==SamplerSettings::FILTERMODE_LINEAR){ + data = read3DInterpolated(position - settings.cellCenterOffset, buf, buf_dimensions); + } + else if(FM==SamplerSettings::FILTERMODE_NEAREST){ //NEAREST + data = read3DNearest(position - settings.cellCenterOffset, buf, buf_dimensions); + } + else if(FM==SamplerSettings::FILTERMODE_MIN){ + data = read3DMin(position - settings.cellCenterOffset, buf, buf_dimensions); + } + else if(FM==SamplerSettings::FILTERMODE_MAX){ + data = read3DMax(position - settings.cellCenterOffset, buf, buf_dimensions); + } + return data; +} + +template +__device__ inline T_DATA read3D(const SamplerSettings settings, const Grid3D input, const glm::vec3 position){ + return _internal_fetch3D(settings, position, input.dimensions, input.d_data); +} + +template +__device__ inline T_DATA read3DLevel(const SamplerSettings settings, const Grid3D input, const glm::vec3 position, const int32_t mipLevel){ + const glm::ivec3 mipDims = input.dimensions>>mipLevel; + const glm::vec3 mipPos = position/static_cast(1<(settings, mipPos, mipDims, input.d_mips[mipLevel]); +} + +template +__device__ inline T_DATA sample3D(const SamplerSettings settings, const Grid3D input, const glm::vec4 position){ + if(MM==SamplerSettings::MIPMODE_NONE){ + return read3D(settings, input, position); + }else{ + const float lod = getLoD(settings, position); + //if NONE: input, if NEAREST: mip[int(mip+0.5)], if LINEAR: mip[int(mip)] + const int32_t mipLevelFloor = getMipLevelFloor(lod, input.mipLevel); + T_DATA data = read3DLevel(settings, input, position, mipLevelFloor); + + if(MM==SamplerSettings::MIPMODE_LINEAR){ + const int32_t mipLevelCeil = getMipLevelCeil(lod, input.mipLevel); + if(mipLevelCeil>mipLevelFloor){ + //const T_DATA *mipCeil = mips[mipLevelCeil]; + data = vmath::lerp(data, read3DLevel(settings, input, position, mipLevelCeil), glm::fract(lod)); + } + } + return data; + } +} + +// --- Data Gradients --- + +template +__device__ inline DataGrad3D _internal_fetchGrad3D(const SamplerSettings settings, const glm::vec3 position, const glm::ivec3 buf_dimensions, const T_DATA *buf){ + DataGrad3D data = {0}; + // undo center offset to +0.0 as needed by the interpolation + //position -= settings.cellCenterOffset; + if(FM==SamplerSettings::FILTERMODE_LINEAR){ + data = read3DGrad(position - settings.cellCenterOffset, buf, buf_dimensions); + } + else{ //NEAREST, MIN, MAX + // return 0 as data gradient for step functions. A linear data gradient might be useful for NN sampling? + } + return data; +} +template +__device__ inline DataGrad3D readGrad3D(const SamplerSettings settings, const Grid3D input, const glm::vec3 position){ + return _internal_fetchGrad3D(settings, position, input.dimensions, input.d_data); +} +template +__device__ inline DataGrad3D readGrad3DLevel(const SamplerSettings settings, const Grid3D input, const glm::vec3 position, int32_t mipLevel){ + const glm::ivec3 mipDims = input.dimensions>>mipLevel; + const glm::vec3 mipPos = position/static_cast(1<(settings, mipPos, mipDims, input.d_mips[mipLevel]); +} + +//return x, y, z data gradient at the sample location (0 for NN) +template +__device__ inline DataGrad3D sampleGrad3D(const SamplerSettings settings, const Grid3D input, const glm::vec4 position){ + if(settings.mipMode==SamplerSettings::MIPMODE_NONE){ + return readGrad3D(settings, input, position); + }else{ + const float lod = getLoD(settings, position); + //if NONE: input, if NEAREST: mip[int(mip+0.5)], if LINEAR: mip[int(mip)] + const int32_t mipLevelFloor = getMipLevelFloor(lod, input.mipLevel); + DataGrad3D data = readGrad3DLevel(settings, input, position, mipLevelFloor); + + if(settings.mipMode==SamplerSettings::MIPMODE_LINEAR){ + const int32_t mipLevelCeil = getMipLevelCeil(lod, input.mipLevel); + if(mipLevelCeil>mipLevelFloor){ + //const T_DATA *mipCeil = mips[mipLevelCeil]; + float lodWeight = glm::fract(lod); + DataGrad3D data2 = readGrad3DLevel(settings, input, position, mipLevelCeil); + data.dx = vmath::lerp(data.dx,data2.dx, lodWeight); + data.dy = vmath::lerp(data.dy,data2.dy, lodWeight); + data.dz = vmath::lerp(data.dz,data2.dz, lodWeight); + } + } + return data; + } +} + +} //Sampling +#endif //_INCLUDE_SAMPLING \ No newline at end of file diff --git a/phitest/render/cuda/src/sampling_settings.hpp b/phitest/render/cuda/src/sampling_settings.hpp new file mode 100644 index 0000000..85a851e --- /dev/null +++ b/phitest/render/cuda/src/sampling_settings.hpp @@ -0,0 +1,74 @@ +#pragma once + +#ifndef _INCLUDE_SAMPLING_SETTIGNS +#define _INCLUDE_SAMPLING_SETTIGNS + +//#include "glm/vec3.hpp" +//#include "glm/vec4.hpp" + +//#include "vector_io.hpp" + +#ifdef __CUDACC__ +#define CUDA_QUALIFIER __host__ __device__ +#else +#define CUDA_QUALIFIER +#endif + +namespace Sampling{ + +struct SamplerSettings{ + enum MipMode{MIPMODE_NONE=0, MIPMODE_NEAREST=1, MIPMODE_LINEAR=2} mipMode; + enum FilterMode{FILTERMODE_NEAREST=0, FILTERMODE_LINEAR=1, FILTERMODE_MIN=2, FILTERMODE_MAX=3, FILTERMODE_QUADRATIC=4} filterMode; //FILTERMODE_MIN FILTERMODE_MAX + bool useTexture; + enum BoundaryMode{BORDER=0, CLAMP=1, WRAP=2, MIRROR=3} boundaryMode; + float cellCenterOffset; + int32_t mipLevel; + float mipClampMin, mipClampMax, mipBias; +}; +CUDA_QUALIFIER constexpr bool usesMip(const SamplerSettings &settings){ return settings.mipMode != SamplerSettings::MIPMODE_NONE;} +CUDA_QUALIFIER constexpr bool usesTexture(const SamplerSettings &settings){ return settings.useTexture;} + + +enum SamplingMode : uint32_t{ + NEAREST = 0b0000, + LINEAR = 0b0001, + NEAREST_MIP_NEAREST = 0b0100, + NEAREST_MIP_LINEAR = 0b0110, + LINEAR_MIP_NEAREST = 0b0101, + LINEAR_MIP_LINEAR = 0b0101, + + TEX_NEAREST = 0b1000, + TEX_LINEAR = 0b1001, + TEX_NEAREST_MIP_NEAREST = 0b1100, + TEX_NEAREST_MIP_LINEAR = 0b1110, + TEX_LINEAR_MIP_NEAREST = 0b1101, + TEX_LINEAR_MIP_LINEAR = 0b1101, + + //helpers + _MIP = 0b0100, + _MIP_LINEAR = 0b0010, + _TEX = 0b1000, + _TEX_MIP = 0b1100, +}; + +CUDA_QUALIFIER constexpr uint32_t mipFlag(const SamplingMode flags){ return (_MIP & flags);} +CUDA_QUALIFIER constexpr bool usesMip(const SamplingMode flags){ return mipFlag(flags) !=0;} + +CUDA_QUALIFIER constexpr uint32_t textureFlag(const SamplingMode flags){ return (_TEX & flags);} +CUDA_QUALIFIER constexpr bool usesTexture(const SamplingMode flags){ return textureFlag(flags) !=0;} + + +enum CoordinateMode{ + TransformLinDepth, + TransformLinDepthReverse, + TransformLinDepthDoubleReverse, + Transform, + TransformReverse, + PixelRaysView, + PixelRaysWorld, + LuT, +}; + + +} //Sampling +#endif //_INCLUDE_SAMPLING_SETTIGNS \ No newline at end of file diff --git a/phitest/render/cuda/src/sampling_settings_v2.hpp b/phitest/render/cuda/src/sampling_settings_v2.hpp new file mode 100644 index 0000000..d13b8b5 --- /dev/null +++ b/phitest/render/cuda/src/sampling_settings_v2.hpp @@ -0,0 +1,16 @@ +#pragma once + +#ifndef _INCLUDE_SAMPLING_SETTINGS_2 +#define _INCLUDE_SAMPLING_SETTINGS_2 + +//#include + +namespace Sampling{ + +enum BoundaryMode{BOUNDARY_BORDER=0, BOUNDARY_CLAMP=1, BOUNDARY_WRAP=2}; //, BOUNDARY_MIRROR=3 +enum FilterMode{FILTERMODE_NEAREST=0, FILTERMODE_LINEAR=1, FILTERMODE_MIN=2, FILTERMODE_MAX=3}; //, BOUNDARY_MIRROR=3 + +} //Sampling + + +#endif //_INCLUDE_SAMPLING_SETTINGS_2 \ No newline at end of file diff --git a/phitest/render/cuda/src/sampling_v2.hpp b/phitest/render/cuda/src/sampling_v2.hpp new file mode 100644 index 0000000..c4dd1bf --- /dev/null +++ b/phitest/render/cuda/src/sampling_v2.hpp @@ -0,0 +1,834 @@ +#pragma once + +#ifndef _INCLUDE_SAMPLING_2 +#define _INCLUDE_SAMPLING_2 + +#include"sampling_settings_v2.hpp" + +#include "vectormath.hpp" +#include "vector_io.hpp" +#include "bounds_checks.hpp" + +namespace Sampling{ + +template +inline __device__ T read3D(const bool valid, const int32_t positionX, const int32_t positionY, const int32_t positionZ, const T* buf, const int3 buf_dims, const float constantValue){ + if(valid){ + const T data = vectorIO::readVectorType3D(positionX, positionY, positionZ, buf_dims, buf); + return data; + }else{ + const T data = vmath::make_cudaFloat(constantValue); + return data; + } + +} + +__host__ __device__ inline bool isInBounds3D(const float3 pos, const int3 dims){ + return 0<=pos.x && pos.x +__device__ inline T read3DInterpolated(const float3 position, const T* buf, const int3 buf_dims, const float constantValue){ + + //weights for ceil, floor weights are (1-weights) + const float3 weights = fracf(position); + + //calculate corner indices + int3 ceilIdx = make_int3(ceilf(position)); + int3 floorIdx = make_int3(floorf(position)); + T data = vmath::make_cudaFloat(constantValue); + + if (BM == BoundaryMode::BOUNDARY_BORDER){// const value outside domain + if(!isInBounds3D(position+0.5f, buf_dims)){ + return vmath::make_cudaFloat(constantValue); + } + int3 ceilValid = (-1 < ceilIdx)*( ceilIdx < buf_dims); + int3 floorValid = (-1 < floorIdx)*(floorIdx < buf_dims); +# define READ_EXTREMA(X,Y,Z) read3D((X##Valid.x*Y##Valid.y*Z##Valid.z)>0.f, X##Idx.x, Y##Idx.y, Z##Idx.z, buf, buf_dims, constantValue) + //read and interpolate along x + const T v00 = lerp( + READ_EXTREMA(floor, floor, floor), + READ_EXTREMA( ceil, floor, floor), + weights.x); + const T v01 = lerp( + READ_EXTREMA(floor, ceil, floor), + READ_EXTREMA( ceil, ceil, floor), + weights.x); + //interpolate along y + const T v0 = lerp(v00, v01, weights.y); + + + const T v10 = lerp( + READ_EXTREMA(floor, floor, ceil), + READ_EXTREMA( ceil, floor, ceil), + weights.x); + const T v11 = lerp( + READ_EXTREMA(floor, ceil, ceil), + READ_EXTREMA( ceil, ceil, ceil), + weights.x); + //interpolate along y + const T v1 = lerp(v10, v11, weights.y); +# undef READ_EXTREMA + + //interpolate along z + data = lerp(v0, v1, weights.z); //(1-weights.z)*yValue.x + weights.z*yValue.y; + + }else{//here the indices are always valid, so no special handling needed + if(BM == BOUNDARY_CLAMP){ + ceilIdx = clamp(ceilIdx, make_int3(0), buf_dims -1); + floorIdx = clamp(floorIdx, make_int3(0), buf_dims -1); + } + else if (BM == BOUNDARY_WRAP){//periodic + ceilIdx = vmath::positivemod(ceilIdx, buf_dims); + floorIdx = vmath::positivemod(floorIdx, buf_dims); + } +# define READ_EXTREMA(X,Y,Z) read3D(true, X##Idx.x, Y##Idx.y, Z##Idx.z, buf, buf_dims, constantValue) + //read and interpolate along x + const T v00 = lerp( + READ_EXTREMA(floor, floor, floor), + READ_EXTREMA( ceil, floor, floor), + weights.x); + const T v01 = lerp( + READ_EXTREMA(floor, ceil, floor), + READ_EXTREMA( ceil, ceil, floor), + weights.x); + //interpolate along y + const T v0 = lerp(v00, v01, weights.y); + + + const T v10 = lerp( + READ_EXTREMA(floor, floor, ceil), + READ_EXTREMA( ceil, floor, ceil), + weights.x); + const T v11 = lerp( + READ_EXTREMA(floor, ceil, ceil), + READ_EXTREMA( ceil, ceil, ceil), + weights.x); + //interpolate along y + const T v1 = lerp(v10, v11, weights.y); +# undef READ_EXTREMA + + + //interpolate along z + data = lerp(v0, v1, weights.z); //(1-weights.z)*yValue.x + weights.z*yValue.y; + } + + return data; +} +template +__device__ inline T read3DNearest(const float3 position, const T* buf, const int3 buf_dims, const float constantValue){ + int3 idx = make_int3(position +0.5f); + if (BM == BOUNDARY_BORDER){ + if(!isInBounds3D(idx, buf_dims)){ + return vmath::make_cudaFloat(constantValue); + } + } + else if(BM == BOUNDARY_CLAMP){ + idx = clamp(idx, make_int3(0), buf_dims - 1); + } + else if (BM == BOUNDARY_WRAP){//periodic + idx = vmath::pmod(idx, buf_dims); + } + return vectorIO::readVectorType3D(idx, buf_dims, buf); +} + +template +struct DataWithExtrema{ + T data; + T min; + T max; +}; + +template +inline __device__ T read3DWithExtrema(const bool valid, const int32_t positionX, const int32_t positionY, const int32_t positionZ, DataWithExtrema& dataExtrema, const T* buf, const int3 buf_dims, const float constantValue){ + if(valid){ + const T data = vectorIO::readVectorType3D(positionX, positionY, positionZ, buf_dims, buf); + dataExtrema.min = vmath::lerp(dataExtrema.min, data, data < dataExtrema.min); + dataExtrema.max = vmath::lerp(dataExtrema.max, data, dataExtrema.max < data); + return data; + }else{ + const T data = vmath::make_cudaFloat(constantValue); + return data; + } + +} + +template +__device__ inline DataWithExtrema read3DInterpolatedWithExtrema(const float3 position, const T* buf, const int3 buf_dims, const float constantValue){ + + //weights for ceil, floor weights are (1-weights) + const float3 weights = fracf(position); + + //calculate corner indices + int3 ceilIdx = make_int3(ceilf(position)); + int3 floorIdx = make_int3(floorf(position)); + DataWithExtrema data; + data.data = vmath::make_cudaFloat(constantValue); + data.min = vmath::make_cudaFloat(FLOAT_MAX); + data.max = vmath::make_cudaFloat(-FLOAT_MAX); + + if (BM == BOUNDARY_BORDER){// const 0 outside domain + if(!isInBounds3D(position+0.5f, buf_dims)){ + return data; + } + int3 ceilValid = (-1 < ceilIdx)*( ceilIdx < buf_dims); + int3 floorValid = (-1 < floorIdx)*(floorIdx < buf_dims); +# define READ_EXTREMA(X,Y,Z) read3DWithExtrema((X##Valid.x*Y##Valid.y*Z##Valid.z)>0.f, X##Idx.x, Y##Idx.y, Z##Idx.z, data, buf, buf_dims, constantValue) + //read and interpolate along x + const T v00 = lerp( + READ_EXTREMA(floor, floor, floor), + READ_EXTREMA( ceil, floor, floor), + weights.x); + const T v01 = lerp( + READ_EXTREMA(floor, ceil, floor), + READ_EXTREMA( ceil, ceil, floor), + weights.x); + //interpolate along y + const T v0 = lerp(v00, v01, weights.y); + + + const T v10 = lerp( + READ_EXTREMA(floor, floor, ceil), + READ_EXTREMA( ceil, floor, ceil), + weights.x); + const T v11 = lerp( + READ_EXTREMA(floor, ceil, ceil), + READ_EXTREMA( ceil, ceil, ceil), + weights.x); + //interpolate along y + const T v1 = lerp(v10, v11, weights.y); +# undef READ_EXTREMA + + //interpolate along z + data.data = lerp(v0, v1, weights.z); //(1-weights.z)*yValue.x + weights.z*yValue.y; + + }else{//here the indices are always valid, so no special handling needed + if(BM == BOUNDARY_CLAMP){ + ceilIdx = clamp(ceilIdx, make_int3(0), buf_dims -1); + floorIdx = clamp(floorIdx, make_int3(0), buf_dims -1); + } + else if (BM == BOUNDARY_WRAP){//periodic + ceilIdx = vmath::positivemod(ceilIdx, buf_dims); + floorIdx = vmath::positivemod(floorIdx, buf_dims); + } +# define READ_EXTREMA(X,Y,Z) read3DWithExtrema(true, X##Idx.x, Y##Idx.y, Z##Idx.z, data, buf, buf_dims, constantValue) + //read and interpolate along x + const T v00 = lerp( + READ_EXTREMA(floor, floor, floor), + READ_EXTREMA( ceil, floor, floor), + weights.x); + const T v01 = lerp( + READ_EXTREMA(floor, ceil, floor), + READ_EXTREMA( ceil, ceil, floor), + weights.x); + //interpolate along y + const T v0 = lerp(v00, v01, weights.y); + + + const T v10 = lerp( + READ_EXTREMA(floor, floor, ceil), + READ_EXTREMA( ceil, floor, ceil), + weights.x); + const T v11 = lerp( + READ_EXTREMA(floor, ceil, ceil), + READ_EXTREMA( ceil, ceil, ceil), + weights.x); + //interpolate along y + const T v1 = lerp(v10, v11, weights.y); +# undef READ_EXTREMA + + + //interpolate along z + data.data = lerp(v0, v1, weights.z); //(1-weights.z)*yValue.x + weights.z*yValue.y; + } + + return data; +} + +// spatial data gradients + +template +struct DataGrad3D{ + T dx,dz,dy; +}; +template +__device__ inline DataGrad3D read3DGrad(const float3 position, const T* buf, const int3 buf_dims, const float constantValue){ + + //weights for ceil, floor weights are (1-weights) + const float3 weights = fracf(position); + + //calculate corner indices + int3 ceilIdx = make_int3(ceilf(position)); + int3 floorIdx = make_int3(floorf(position)); + + if (BM == BOUNDARY_BORDER){// constant value outside domain + if(!isInBounds3D(position+0.5f, buf_dims)){ + DataGrad3D zero = {0};// 0 grad due to constant value + return zero; + } + int3 ceilValid = (-1 < ceilIdx)*( ceilIdx < buf_dims); + int3 floorValid = (-1 < floorIdx)*(floorIdx < buf_dims); + T data = vmath::make_cudaFloat(constantValue); + //read + const T fxfyfz = (floorValid.x*floorValid.y*floorValid.z) + ? vectorIO::readVectorType3D(floorIdx.x, floorIdx.y, floorIdx.z, buf_dims, buf) + : data; + const T cxfyfz = ( ceilValid.x*floorValid.y*floorValid.z) + ? vectorIO::readVectorType3D( ceilIdx.x, floorIdx.y, floorIdx.z, buf_dims, buf) + : data; + const T fxcyfz = (floorValid.x* ceilValid.y*floorValid.z) + ? vectorIO::readVectorType3D(floorIdx.x, ceilIdx.y, floorIdx.z, buf_dims, buf) + : data; + const T cxcyfz = ( ceilValid.x* ceilValid.y*floorValid.z) + ? vectorIO::readVectorType3D( ceilIdx.x, ceilIdx.y, floorIdx.z, buf_dims, buf) + : data; + const T fxfycz = (floorValid.x*floorValid.y* ceilValid.z) + ? vectorIO::readVectorType3D(floorIdx.x, floorIdx.y, ceilIdx.z, buf_dims, buf) + : data; + const T cxfycz = ( ceilValid.x*floorValid.y* ceilValid.z) + ? vectorIO::readVectorType3D( ceilIdx.x, floorIdx.y, ceilIdx.z, buf_dims, buf) + : data; + const T fxcycz = (floorValid.x* ceilValid.y* ceilValid.z) + ? vectorIO::readVectorType3D(floorIdx.x, ceilIdx.y, ceilIdx.z, buf_dims, buf) + : data; + const T cxcycz = ( ceilValid.x* ceilValid.y* ceilValid.z) + ? vectorIO::readVectorType3D( ceilIdx.x, ceilIdx.y, ceilIdx.z, buf_dims, buf) + : data; + //interpolate differences + DataGrad3D dataGrad; + dataGrad.dx = lerp( + lerp((cxfyfz-fxfyfz),(cxcyfz-fxcyfz), weights.y), + lerp((cxfycz-fxfycz),(cxcycz-fxcycz), weights.y), + weights.z); + dataGrad.dy = lerp( + lerp((fxcyfz-fxfyfz),(cxcyfz-cxfyfz), weights.x), + lerp((fxcycz-fxfycz),(cxcycz-cxfycz), weights.x), + weights.z); + dataGrad.dz = lerp( + lerp((fxfycz-fxfyfz),(cxfycz-cxfyfz), weights.x), + lerp((fxcycz-fxcyfz),(cxcycz-cxcyfz), weights.x), + weights.y); + + return dataGrad; + + }else{//here the indices are always valid, so no special handling needed + if(BM == BOUNDARY_CLAMP){ + ceilIdx = clamp(ceilIdx, make_int3(0), buf_dims -1); + floorIdx = clamp(floorIdx, make_int3(0), buf_dims -1); + } + else if (BM == BOUNDARY_WRAP){//periodic + ceilIdx = vmath::positivemod(ceilIdx, buf_dims); + floorIdx = vmath::positivemod(floorIdx, buf_dims); + } + //read + const T fxfyfz = vectorIO::readVectorType3D(floorIdx.x, floorIdx.y, floorIdx.z, buf_dims, buf); + const T cxfyfz = vectorIO::readVectorType3D( ceilIdx.x, floorIdx.y, floorIdx.z, buf_dims, buf); + const T fxcyfz = vectorIO::readVectorType3D(floorIdx.x, ceilIdx.y, floorIdx.z, buf_dims, buf); + const T cxcyfz = vectorIO::readVectorType3D( ceilIdx.x, ceilIdx.y, floorIdx.z, buf_dims, buf); + const T fxfycz = vectorIO::readVectorType3D(floorIdx.x, floorIdx.y, ceilIdx.z, buf_dims, buf); + const T cxfycz = vectorIO::readVectorType3D( ceilIdx.x, floorIdx.y, ceilIdx.z, buf_dims, buf); + const T fxcycz = vectorIO::readVectorType3D(floorIdx.x, ceilIdx.y, ceilIdx.z, buf_dims, buf); + const T cxcycz = vectorIO::readVectorType3D( ceilIdx.x, ceilIdx.y, ceilIdx.z, buf_dims, buf); + //interpolate differences + DataGrad3D dataGrad; + dataGrad.dx = lerp( + lerp((cxfyfz-fxfyfz),(cxcyfz-fxcyfz), weights.y), + lerp((cxfycz-fxfycz),(cxcycz-fxcycz), weights.y), + weights.z); + dataGrad.dy = lerp( + lerp((fxcyfz-fxfyfz),(cxcyfz-cxfyfz), weights.x), + lerp((fxcycz-fxfycz),(cxcycz-cxfycz), weights.x), + weights.z); + dataGrad.dz = lerp( + lerp((fxfycz-fxfyfz),(cxfycz-cxfyfz), weights.x), + lerp((fxcycz-fxcyfz),(cxcycz-cxcyfz), weights.x), + weights.y); + + return dataGrad; + } +} + +template +__device__ inline T sample3D(const float3 position, const T* buf, const int3 buf_dims, const float constantValue = 0.f){ + if(FM==FILTERMODE_LINEAR){ + return read3DInterpolated(position - 0.5f, buf, buf_dims, constantValue); + }else if(FM==FILTERMODE_NEAREST){ + return read3DNearest(position - 0.5f, buf, buf_dims, constantValue); + }else if(FM==FILTERMODE_MIN){ + return read3DInterpolatedWithExtrema(position - 0.5f, buf, buf_dims, constantValue).min; + }else if(FM==FILTERMODE_MAX){ + return read3DInterpolatedWithExtrema(position - 0.5f, buf, buf_dims, constantValue).max; + }else{ + T data{}; + return data; + } +} +template +__device__ inline DataGrad3D sample3DGrad(const float3 position, const T* buf, const int3 buf_dims, const float constantValue = 0.f){ + if(FM==FILTERMODE_LINEAR){ + return read3DGrad(position - 0.5f, buf, buf_dims, constantValue); + }/* else if(FM==FILTERMODE_NEAREST){ + return read3DNearest(position - 0.5f, buf, buf_dims); + }else if(FM==FILTERMODE_MIN){ + return read3DInterpolatedWithExtrema(position - 0.5f, buf, buf_dims).min; + }else if(FM==FILTERMODE_MAX){ + return read3DInterpolatedWithExtrema(position - 0.5f, buf, buf_dims).max; + } */else{ + DataGrad3D data{}; + return data; + } +} + +// gradient scattering + +/* +// position is without offset (+0.0) +template +__device__ inline void scatterGrad3DInterpolated(const T out_grad, const float3 position, T* input_grad, const int3 buf_dims){ + if(BM!=BOUNDARY_BORDER || (CHECK_BOUNDS_SV3V3(-1.f, <, position, <, buf_dims))){ + const float3 cw = fracf(position); + const float3 fw = 1.f - cw; + int3 ceilIdx = make_int3(ceilf(position)); + int3 floorIdx = make_int3(floorf(position)); + + if(BM==BOUNDARY_CLAMP){ + ceilIdx = clamp(ceilIdx, make_int3(0), buf_dims -1); + floorIdx = clamp(floorIdx, make_int3(0), buf_dims -1); + }else if(BM==BOUNDARY_WRAP){ + ceilIdx = vmath::positivemod(ceilIdx, buf_dims); + floorIdx = vmath::positivemod(floorIdx, buf_dims); + } + + //accumulate weighted gradients + if(BM!=BOUNDARY_BORDER || (floorIdx.x>=0 && floorIdx.y>=0 && floorIdx.z>=0)){ + vectorIO::atomicAddVectorType3D(out_grad*(fw.x*fw.y*fw.z), floorIdx.x, floorIdx.y, floorIdx.z, buf_dims, input_grad); + } + if(BM!=BOUNDARY_BORDER || (ceilIdx.x=0 && floorIdx.z>=0)){ + vectorIO::atomicAddVectorType3D(out_grad*(cw.x*fw.y*fw.z), ceilIdx.x, floorIdx.y, floorIdx.z, buf_dims, input_grad); + } + if(BM!=BOUNDARY_BORDER || (floorIdx.x>=0 && ceilIdx.y=0)){ + vectorIO::atomicAddVectorType3D(out_grad*(fw.x*cw.y*fw.z), floorIdx.x, ceilIdx.y, floorIdx.z, buf_dims, input_grad); + } + if(BM!=BOUNDARY_BORDER || (ceilIdx.x=0)){ + vectorIO::atomicAddVectorType3D(out_grad*(cw.x*cw.y*fw.z), ceilIdx.x, ceilIdx.y, floorIdx.z, buf_dims, input_grad); + } + + if(BM!=BOUNDARY_BORDER || (floorIdx.x>=0 && floorIdx.y>=0 && ceilIdx.z(out_grad*(fw.x*fw.y*cw.z), floorIdx.x, floorIdx.y, ceilIdx.z, buf_dims, input_grad); + } + if(BM!=BOUNDARY_BORDER || (ceilIdx.x=0 && ceilIdx.z(out_grad*(cw.x*fw.y*cw.z), ceilIdx.x, floorIdx.y, ceilIdx.z, buf_dims, input_grad); + } + if(BM!=BOUNDARY_BORDER || (floorIdx.x>=0 && ceilIdx.y(out_grad*(fw.x*cw.y*cw.z), floorIdx.x, ceilIdx.y, ceilIdx.z, buf_dims, input_grad); + } + if(BM!=BOUNDARY_BORDER || (ceilIdx.x(out_grad*(cw.x*cw.y*cw.z), ceilIdx.x, ceilIdx.y, ceilIdx.z, buf_dims, input_grad); + } + } +}*/ +// position is without offset (+0.0) +template +struct SingleSampleContribution{ + __device__ inline static void scatter(const T data, const int3 idx, const float w, T* buf, C* num_samples, const int3 buf_dims); +}; +template +struct SingleSampleContribution{ + __device__ inline static void scatter(const T data, const int3 idx, const float w, T* buf, void* num_samples, const int3 buf_dims){ + if(BM!=BOUNDARY_BORDER || (CHECK_BOUNDS_SV3V3(0, <=, idx, <, buf_dims))){ + const size_t flatIdx3D = vectorIO::flatIdx3D(idx.x, idx.y, idx.z, buf_dims); + vectorIO::atomicAddVectorTypeAbs(data*w, flatIdx3D, buf); + } + } +}; +template +struct SingleSampleContribution{ + __device__ inline static void scatter(const T data, const int3 idx, const float w, T* buf, uint32_t* num_samples, const int3 buf_dims){ + if(BM!=BOUNDARY_BORDER || (CHECK_BOUNDS_SV3V3(0, <=, idx, <, buf_dims))){ + const size_t flatIdx3D = vectorIO::flatIdx3D(idx.x, idx.y, idx.z, buf_dims); + vectorIO::atomicAddVectorTypeAbs(data*w, flatIdx3D, buf); + atomicInc(num_samples + flatIdx3D, 0xffffffff); + } + } +}; +template +struct SingleSampleContribution{ + __device__ inline static void scatter(const T data, const int3 idx, const float w, T* buf, float* num_samples, const int3 buf_dims){ + if(BM!=BOUNDARY_BORDER + || (CHECK_BOUNDS_SV3V3(0, <=, idx, <, buf_dims))){ + const size_t flatIdx3D = vectorIO::flatIdx3D(idx.x, idx.y, idx.z, buf_dims); + vectorIO::atomicAddVectorTypeAbs(data*w, flatIdx3D, buf); + atomicAdd(num_samples + flatIdx3D, w); + } + } +}; + +template +__device__ inline void scatterGrad3DInterpolated(const T out_grad, const float3 position, T* input_grad, C* num_samples, const int3 buf_dims){ + if(BM!=BOUNDARY_BORDER || (CHECK_BOUNDS_SV3V3(-1.f, <, position, <, buf_dims))){ + const float3 cw = fracf(position); + const float3 fw = 1.f - cw; + int3 ceilIdx = make_int3(ceilf(position)); + int3 floorIdx = make_int3(floorf(position)); + + if(BM==BOUNDARY_CLAMP){ + ceilIdx = clamp(ceilIdx, make_int3(0), buf_dims -1); + floorIdx = clamp(floorIdx, make_int3(0), buf_dims -1); + }else if(BM==BOUNDARY_WRAP){ + ceilIdx = vmath::pmod(ceilIdx, buf_dims); + floorIdx = vmath::pmod(floorIdx, buf_dims); + } + + //accumulate weighted gradients + SingleSampleContribution::scatter(out_grad, make_int3(floorIdx.x, floorIdx.y, floorIdx.z), (fw.x*fw.y*fw.z), input_grad, num_samples, buf_dims); + SingleSampleContribution::scatter(out_grad, make_int3(ceilIdx.x, floorIdx.y, floorIdx.z), (cw.x*fw.y*fw.z), input_grad, num_samples, buf_dims); + SingleSampleContribution::scatter(out_grad, make_int3(floorIdx.x, ceilIdx.y, floorIdx.z), (fw.x*cw.y*fw.z), input_grad, num_samples, buf_dims); + SingleSampleContribution::scatter(out_grad, make_int3(ceilIdx.x, ceilIdx.y, floorIdx.z), (cw.x*cw.y*fw.z), input_grad, num_samples, buf_dims); + + SingleSampleContribution::scatter(out_grad, make_int3(floorIdx.x, floorIdx.y, ceilIdx.z), (fw.x*fw.y*cw.z), input_grad, num_samples, buf_dims); + SingleSampleContribution::scatter(out_grad, make_int3(ceilIdx.x, floorIdx.y, ceilIdx.z), (cw.x*fw.y*cw.z), input_grad, num_samples, buf_dims); + SingleSampleContribution::scatter(out_grad, make_int3(floorIdx.x, ceilIdx.y, ceilIdx.z), (fw.x*cw.y*cw.z), input_grad, num_samples, buf_dims); + SingleSampleContribution::scatter(out_grad, make_int3(ceilIdx.x, ceilIdx.y, ceilIdx.z), (cw.x*cw.y*cw.z), input_grad, num_samples, buf_dims); + /* + if(BM!=BOUNDARY_BORDER || (floorIdx.x>=0 && floorIdx.y>=0 && floorIdx.z>=0)){ + const float w = (fw.x*fw.y*fw.z); + const size_t flatIdx3D = vectorIO::flatIdx3D(floorIdx.x, floorIdx.y, floorIdx.z, buf_dims); + vectorIO::atomicAddVectorType3D(out_grad*w, floorIdx.x, floorIdx.y, floorIdx.z, buf_dims, input_grad); + if(CountSamples==1){ atomicInc(num_samples + flatIdx3D, 0xffffffff); } + else if(CountSamples==2){ atomicAdd(num_samples + flatIdx3D, w); } + } + if(BM!=BOUNDARY_BORDER || (ceilIdx.x=0 && floorIdx.z>=0)){ + const float w = (cw.x*fw.y*fw.z); + const size_t flatIdx3D = vectorIO::flatIdx3D(ceilIdx.x, floorIdx.y, floorIdx.z, buf_dims); + vectorIO::atomicAddVectorType3D(out_grad*w, ceilIdx.x, floorIdx.y, floorIdx.z, buf_dims, input_grad); + if(CountSamples==1){ atomicInc(num_samples + flatIdx3D, 0xffffffff); } + else if(CountSamples==2){ atomicAdd(num_samples + flatIdx3D, w); } + } + if(BM!=BOUNDARY_BORDER || (floorIdx.x>=0 && ceilIdx.y=0)){ + const float w = (fw.x*cw.y*fw.z); + const size_t flatIdx3D = vectorIO::flatIdx3D(floorIdx.x, ceilIdx.y, floorIdx.z, buf_dims); + vectorIO::atomicAddVectorType3D(out_grad*w, floorIdx.x, ceilIdx.y, floorIdx.z, buf_dims, input_grad); + if(CountSamples==1){ atomicInc(num_samples + flatIdx3D, 0xffffffff); } + else if(CountSamples==2){ atomicAdd(num_samples + flatIdx3D, w); } + } + if(BM!=BOUNDARY_BORDER || (ceilIdx.x=0)){ + const float w = (cw.x*cw.y*fw.z); + const size_t flatIdx3D = vectorIO::flatIdx3D(ceilIdx.x, ceilIdx.y, floorIdx.z, buf_dims); + vectorIO::atomicAddVectorType3D(out_grad*w, ceilIdx.x, ceilIdx.y, floorIdx.z, buf_dims, input_grad); + if(CountSamples==1){ atomicInc(num_samples + flatIdx3D, 0xffffffff); } + else if(CountSamples==2){ atomicAdd(num_samples + flatIdx3D, w); } + } + + if(BM!=BOUNDARY_BORDER || (floorIdx.x>=0 && floorIdx.y>=0 && ceilIdx.z(out_grad*w, floorIdx.x, floorIdx.y, ceilIdx.z, buf_dims, input_grad); + if(CountSamples==1){ atomicInc(num_samples + flatIdx3D, 0xffffffff); } + else if(CountSamples==2){ atomicAdd(num_samples + flatIdx3D, w); } + } + if(BM!=BOUNDARY_BORDER || (ceilIdx.x=0 && ceilIdx.z(out_grad*w, ceilIdx.x, floorIdx.y, ceilIdx.z, buf_dims, input_grad); + if(CountSamples==1){ atomicInc(num_samples + flatIdx3D, 0xffffffff); } + else if(CountSamples==2){ atomicAdd(num_samples + flatIdx3D, w); } + } + if(BM!=BOUNDARY_BORDER || (floorIdx.x>=0 && ceilIdx.y(out_grad*w, floorIdx.x, ceilIdx.y, ceilIdx.z, buf_dims, input_grad); + if(CountSamples==1){ atomicInc(num_samples + flatIdx3D, 0xffffffff); } + else if(CountSamples==2){ atomicAdd(num_samples + flatIdx3D, w); } + } + if(BM!=BOUNDARY_BORDER || (ceilIdx.x(out_grad*w, ceilIdx.x, ceilIdx.y, ceilIdx.z, buf_dims, input_grad); + if(CountSamples==1){ atomicInc(num_samples + flatIdx3D, 0xffffffff); } + else if(CountSamples==2){ atomicAdd(num_samples + flatIdx3D, w); } + } + */ + } +} + +template +__device__ inline void scatter3D(const T data, const float3 position, T* buf, const int3 buf_dims, C* num_samples){ + if(FM==FILTERMODE_LINEAR){ + scatterGrad3DInterpolated(data, position - 0.5f, buf, num_samples, buf_dims); + }/*else if(FM==FILTERMODE_NEAREST){ + scatterGrad3DNearest(position - 0.5f, buf, buf_dims); + }else if(FM==FILTERMODE_MIN){ + return read3DInterpolatedWithExtrema(position - 0.5f, buf, buf_dims).min; + }else if(FM==FILTERMODE_MAX){ + return read3DInterpolatedWithExtrema(position - 0.5f, buf, buf_dims).max; + }*/ +} + +// gradients of the data gradients + +/* +* d lerp(a,b,t) +* /d a: (1-t); /d b: t; /d t: b-a +* d lerp(lerp(a,b,t1),lerp(c,d,t1),t2) +* /d a: [d lerp(...,t2)/d lerp(a,b,t1)]*[d lerp(a,b,t1)/d a] = (1-t2)*(1-t1) +* /d t1: (1-t2)*(b-a) + t2*(d-c) = lerp(b-a,d-c,t2) +* /d t2: lerp(c,d,t1) - lerp(a,b,t1) = lerp(c-a,d-b,t1) +*/ + +template +__device__ inline void scatter3DGradDataGradInterpolated(const float3 position, const DataGrad3D dataGradGrad, T* buf, const int3 buf_dims){ + // gradient of data gradient w.r.t. original data + //weights for ceil, floor weights are (1-weights) + const float3 weights = fracf(position); + + //calculate corner indices + int3 ceilIdx = make_int3(ceilf(position)); + int3 floorIdx = make_int3(floorf(position)); + + T fxfyfz = vmath::make_cudaFloat(0.0f); + T cxfyfz = vmath::make_cudaFloat(0.0f); + T fxcyfz = vmath::make_cudaFloat(0.0f); + T cxcyfz = vmath::make_cudaFloat(0.0f); + T fxfycz = vmath::make_cudaFloat(0.0f); + T cxfycz = vmath::make_cudaFloat(0.0f); + T fxcycz = vmath::make_cudaFloat(0.0f); + T cxcycz = vmath::make_cudaFloat(0.0f); + + // for reference + //DataGrad3D dataGrad; + /* dataGrad.dx = lerp( + lerp((cxfyfz-fxfyfz),(cxcyfz-fxcyfz), weights.y), + lerp((cxfycz-fxfycz),(cxcycz-fxcycz), weights.y), + weights.z); */ + // a=(cxfyfz-fxfyfz), t1=weights.y, t2=weights.z + + fxfyfz = fxfyfz - (1-weights.y)*(1-weights.z)*dataGradGrad.dx; + cxfyfz = cxfyfz + (1-weights.y)*(1-weights.z)*dataGradGrad.dx; + fxcyfz = fxcyfz - weights.y *(1-weights.z)*dataGradGrad.dx; + cxcyfz = cxcyfz + weights.y *(1-weights.z)*dataGradGrad.dx; + fxfycz = fxfycz - (1-weights.y)* weights.z *dataGradGrad.dx; + cxfycz = cxfycz + (1-weights.y)* weights.z *dataGradGrad.dx; + fxcycz = fxcycz - weights.y * weights.z *dataGradGrad.dx; + cxcycz = cxcycz + weights.y * weights.z *dataGradGrad.dx; + // => x~sign, y~weight.y, z~weight.z + + /* dataGrad.dy = lerp( + lerp((fxcyfz-fxfyfz),(cxcyfz-cxfyfz), weights.x), + lerp((fxcycz-fxfycz),(cxcycz-cxfycz), weights.x), + weights.z); */ + // => y~sign, x~weight.x, z~weight.z + fxfyfz = fxfyfz - (1-weights.x)*(1-weights.z)*dataGradGrad.dy; + cxfyfz = cxfyfz - weights.x *(1-weights.z)*dataGradGrad.dy; + fxcyfz = fxcyfz + (1-weights.x)*(1-weights.z)*dataGradGrad.dy; + cxcyfz = cxcyfz + weights.x *(1-weights.z)*dataGradGrad.dy; + fxfycz = fxfycz - (1-weights.x)* weights.z *dataGradGrad.dy; + cxfycz = cxfycz - weights.x * weights.z *dataGradGrad.dy; + fxcycz = fxcycz + (1-weights.x)* weights.z *dataGradGrad.dy; + cxcycz = cxcycz + weights.x * weights.z *dataGradGrad.dy; + + /* dataGrad.dz = lerp( + lerp((fxfycz-fxfyfz),(cxfycz-cxfyfz), weights.x), + lerp((fxcycz-fxcyfz),(cxcycz-cxcyfz), weights.x), + weights.y); */ + // => z~sign, x~weight.x, y~weight.y + fxfyfz = fxfyfz - (1-weights.x)*(1-weights.y)*dataGradGrad.dz; + cxfyfz = cxfyfz - weights.x *(1-weights.y)*dataGradGrad.dz; + fxcyfz = fxcyfz - (1-weights.x)* weights.y *dataGradGrad.dz; + cxcyfz = cxcyfz - weights.x * weights.y *dataGradGrad.dz; + fxfycz = fxfycz + (1-weights.x)*(1-weights.y)*dataGradGrad.dz; + cxfycz = cxfycz + weights.x *(1-weights.y)*dataGradGrad.dz; + fxcycz = fxcycz + (1-weights.x)* weights.y *dataGradGrad.dz; + cxcycz = cxcycz + weights.x * weights.y *dataGradGrad.dz; + + // scatter without weighting + SingleSampleContribution::scatter(fxfyfz, make_int3(floorIdx.x, floorIdx.y, floorIdx.z), 1.0f, buf, nullptr, buf_dims); + SingleSampleContribution::scatter(cxfyfz, make_int3( ceilIdx.x, floorIdx.y, floorIdx.z), 1.0f, buf, nullptr, buf_dims); + SingleSampleContribution::scatter(fxcyfz, make_int3(floorIdx.x, ceilIdx.y, floorIdx.z), 1.0f, buf, nullptr, buf_dims); + SingleSampleContribution::scatter(cxcyfz, make_int3( ceilIdx.x, ceilIdx.y, floorIdx.z), 1.0f, buf, nullptr, buf_dims); + SingleSampleContribution::scatter(fxfycz, make_int3(floorIdx.x, floorIdx.y, ceilIdx.z), 1.0f, buf, nullptr, buf_dims); + SingleSampleContribution::scatter(cxfycz, make_int3( ceilIdx.x, floorIdx.y, ceilIdx.z), 1.0f, buf, nullptr, buf_dims); + SingleSampleContribution::scatter(fxcycz, make_int3(floorIdx.x, ceilIdx.y, ceilIdx.z), 1.0f, buf, nullptr, buf_dims); + SingleSampleContribution::scatter(cxcycz, make_int3( ceilIdx.x, ceilIdx.y, ceilIdx.z), 1.0f, buf, nullptr, buf_dims); +} +template +__device__ inline void scatter3DGradDataGrad(const float3 position, const DataGrad3D dataGradGrad, T* buf, const int3 buf_dims){ + if(FM==FILTERMODE_LINEAR){ + scatter3DGradDataGradInterpolated(position - 0.5f, dataGradGrad, buf, buf_dims); + }/*else if(FM==FILTERMODE_NEAREST){ + scatterGrad3DNearest(position - 0.5f, buf, buf_dims); + }else if(FM==FILTERMODE_MIN){ + return read3DInterpolatedWithExtrema(position - 0.5f, buf, buf_dims).min; + }else if(FM==FILTERMODE_MAX){ + return read3DInterpolatedWithExtrema(position - 0.5f, buf, buf_dims).max; + }*/ +} + +template +__device__ inline float3 read3DGradPosGradInterpolated(const float3 position, const T* buf, const int3 buf_dims, const DataGrad3D dataGradGrad, const float constantValue){ + // gradient of data gradient w.r.t. sampling position + //weights for ceil, floor weights are (1-weights) + const float3 weights = fracf(position); + float3 weightsGrad = make_float3(0.0f); + + //calculate corner indices + int3 ceilIdx = make_int3(ceilf(position)); + int3 floorIdx = make_int3(floorf(position)); + + if (BM == BOUNDARY_BORDER){// const 0 outside domain + if(!isInBounds3D(position+0.5f, buf_dims)){ + return weightsGrad; + } + int3 ceilValid = (-1 < ceilIdx)*( ceilIdx < buf_dims); + int3 floorValid = (-1 < floorIdx)*(floorIdx < buf_dims); + T data = vmath::make_cudaFloat(constantValue); + //read + const T fxfyfz = (floorValid.x*floorValid.y*floorValid.z) + ? vectorIO::readVectorType3D(floorIdx.x, floorIdx.y, floorIdx.z, buf_dims, buf) + : data; + const T cxfyfz = ( ceilValid.x*floorValid.y*floorValid.z) + ? vectorIO::readVectorType3D( ceilIdx.x, floorIdx.y, floorIdx.z, buf_dims, buf) + : data; + const T fxcyfz = (floorValid.x* ceilValid.y*floorValid.z) + ? vectorIO::readVectorType3D(floorIdx.x, ceilIdx.y, floorIdx.z, buf_dims, buf) + : data; + const T cxcyfz = ( ceilValid.x* ceilValid.y*floorValid.z) + ? vectorIO::readVectorType3D( ceilIdx.x, ceilIdx.y, floorIdx.z, buf_dims, buf) + : data; + const T fxfycz = (floorValid.x*floorValid.y* ceilValid.z) + ? vectorIO::readVectorType3D(floorIdx.x, floorIdx.y, ceilIdx.z, buf_dims, buf) + : data; + const T cxfycz = ( ceilValid.x*floorValid.y* ceilValid.z) + ? vectorIO::readVectorType3D( ceilIdx.x, floorIdx.y, ceilIdx.z, buf_dims, buf) + : data; + const T fxcycz = (floorValid.x* ceilValid.y* ceilValid.z) + ? vectorIO::readVectorType3D(floorIdx.x, ceilIdx.y, ceilIdx.z, buf_dims, buf) + : data; + const T cxcycz = ( ceilValid.x* ceilValid.y* ceilValid.z) + ? vectorIO::readVectorType3D( ceilIdx.x, ceilIdx.y, ceilIdx.z, buf_dims, buf) + : data; + //interpolation gradients w.r.t. position/weight + + // for reference: + /* DataGrad3D dataGrad; + dataGrad.dx = lerp( + lerp((cxfyfz-fxfyfz),(cxcyfz-fxcyfz), weights.y), + lerp((cxfycz-fxfycz),(cxcycz-fxcycz), weights.y), + weights.z); */ + // a=(cxfyfz-fxfyfz), b=..., t1=y, t2=z + //weightsGrad.y += lerp((cxcyfz-fxcyfz)-(cxfyfz-fxfyfz),(cxcycz-fxcycz)-(cxfycz-fxfycz),weights.z)*vmath::sum(dataGradGrad.dx); // inner weight: back-front each + //weightsGrad.z += lerp((cxfycz-fxfycz)-(cxfyfz-fxfyfz),(cxcycz-fxcycz)-(cxcyfz-fxcyfz),weights.y)*vmath::sum(dataGradGrad.dx); // outer weight: bottom-top each + { + //const float dataGradGradX = vmath::sum(dataGradGrad.dx); + const T a = cxfyfz-fxfyfz; + const T b = cxcyfz-fxcyfz; + const T c = cxfycz-fxfycz; + const T d = cxcycz-fxcycz; + weightsGrad.y += vmath::sum(lerp(b-a,d-c,weights.z)*dataGradGrad.dx); + weightsGrad.z += vmath::sum(lerp(c-a,d-b,weights.y)*dataGradGrad.dx); + } + /* dataGrad.dy = lerp( + lerp((fxcyfz-fxfyfz),(cxcyfz-cxfyfz), weights.x), + lerp((fxcycz-fxfycz),(cxcycz-cxfycz), weights.x), + weights.z); */ + // a=(fxcyfz-fxfyfz), b=..., t1=x, t2=z + // weightsGrad.x += lerp((cxcyfz-cxfyfz)-(fxcyfz-fxfyfz),(cxcycz-cxfycz)-(fxcycz-fxfycz),weights.z); + // weightsGrad.z += lerp((fxcycz-fxfycz)-(fxcyfz-fxfyfz),(cxcycz-cxfycz)-(cxcyfz-cxfyfz),weights.x); + { + //const float dataGradGradY = vmath::sum(dataGradGrad.dy); + const T a = fxcyfz-fxfyfz; + const T b = cxcyfz-cxfyfz; + const T c = fxcycz-fxfycz; + const T d = cxcycz-cxfycz; + weightsGrad.x += vmath::sum(lerp(b-a,d-c,weights.z)*dataGradGrad.dy); + weightsGrad.z += vmath::sum(lerp(c-a,d-b,weights.x)*dataGradGrad.dy); + } + /* dataGrad.dz = lerp( + lerp((fxfycz-fxfyfz),(cxfycz-cxfyfz), weights.x), + lerp((fxcycz-fxcyfz),(cxcycz-cxcyfz), weights.x), + weights.y); */ + // a=(fxfycz-fxfyfz), b=..., t1=x, t2=y + // weightsGrad.x += lerp((cxfycz-cxfyfz)-(fxfycz-fxfyfz),(cxcycz-cxcyfz)-(fxcycz-fxcyfz),weights.y); + // weightsGrad.y += lerp((fxcycz-fxcyfz)-(fxfycz-fxfyfz),(cxcycz-cxcyfz)-(cxfycz-cxfyfz),weights.x); + { + //const float dataGradGradZ = vmath::sum(dataGradGrad.dz); + const T a = fxfycz-fxfyfz; + const T b = cxfycz-cxfyfz; + const T c = fxcycz-fxcyfz; + const T d = cxcycz-cxcyfz; + weightsGrad.x += vmath::sum(lerp(b-a,d-c,weights.y)*dataGradGrad.dz); + weightsGrad.y += vmath::sum(lerp(c-a,d-b,weights.x)*dataGradGrad.dz); + } + + return weightsGrad; + + }else{//here the indices are always valid, so no special handling needed + if(BM == BOUNDARY_CLAMP){ + ceilIdx = clamp(ceilIdx, make_int3(0), buf_dims -1); + floorIdx = clamp(floorIdx, make_int3(0), buf_dims -1); + } + else if (BM == BOUNDARY_WRAP){//periodic + ceilIdx = vmath::positivemod(ceilIdx, buf_dims); + floorIdx = vmath::positivemod(floorIdx, buf_dims); + } + //read + const T fxfyfz = vectorIO::readVectorType3D(floorIdx.x, floorIdx.y, floorIdx.z, buf_dims, buf); + const T cxfyfz = vectorIO::readVectorType3D( ceilIdx.x, floorIdx.y, floorIdx.z, buf_dims, buf); + const T fxcyfz = vectorIO::readVectorType3D(floorIdx.x, ceilIdx.y, floorIdx.z, buf_dims, buf); + const T cxcyfz = vectorIO::readVectorType3D( ceilIdx.x, ceilIdx.y, floorIdx.z, buf_dims, buf); + const T fxfycz = vectorIO::readVectorType3D(floorIdx.x, floorIdx.y, ceilIdx.z, buf_dims, buf); + const T cxfycz = vectorIO::readVectorType3D( ceilIdx.x, floorIdx.y, ceilIdx.z, buf_dims, buf); + const T fxcycz = vectorIO::readVectorType3D(floorIdx.x, ceilIdx.y, ceilIdx.z, buf_dims, buf); + const T cxcycz = vectorIO::readVectorType3D( ceilIdx.x, ceilIdx.y, ceilIdx.z, buf_dims, buf); + + { + const T a = cxfyfz-fxfyfz; + const T b = cxcyfz-fxcyfz; + const T c = cxfycz-fxfycz; + const T d = cxcycz-fxcycz; + weightsGrad.y += vmath::sum(lerp(b-a,d-c,weights.z)*dataGradGrad.dx); + weightsGrad.z += vmath::sum(lerp(c-a,d-b,weights.y)*dataGradGrad.dx); + } + { + const T a = fxcyfz-fxfyfz; + const T b = cxcyfz-cxfyfz; + const T c = fxcycz-fxfycz; + const T d = cxcycz-cxfycz; + weightsGrad.x += vmath::sum(lerp(b-a,d-c,weights.z)*dataGradGrad.dy); + weightsGrad.z += vmath::sum(lerp(c-a,d-b,weights.x)*dataGradGrad.dy); + } + { + const T a = fxfycz-fxfyfz; + const T b = cxfycz-cxfyfz; + const T c = fxcycz-fxcyfz; + const T d = cxcycz-cxcyfz; + weightsGrad.x += vmath::sum(lerp(b-a,d-c,weights.y)*dataGradGrad.dz); + weightsGrad.y += vmath::sum(lerp(c-a,d-b,weights.x)*dataGradGrad.dz); + } + + return weightsGrad; + } +} +template +__device__ inline float3 sample3DGradPosGrad(const float3 position, const T* buf, const int3 buf_dims, const DataGrad3D dataGradGrad, const float constantValue = 0.f){ + if(FM==FILTERMODE_LINEAR){ + return read3DGradPosGradInterpolated(position - 0.5f, buf, buf_dims, dataGradGrad, constantValue); + }/*else if(FM==FILTERMODE_NEAREST){ + scatterGrad3DNearest(position - 0.5f, buf, buf_dims); + }else if(FM==FILTERMODE_MIN){ + return read3DInterpolatedWithExtrema(position - 0.5f, buf, buf_dims).min; + }else if(FM==FILTERMODE_MAX){ + return read3DInterpolatedWithExtrema(position - 0.5f, buf, buf_dims).max; + }*/ +} + + +} //Sampling + +#endif //_INCLUDE_SAMPLING_2 \ No newline at end of file diff --git a/phitest/render/cuda/src/transformations.hpp b/phitest/render/cuda/src/transformations.hpp new file mode 100644 index 0000000..0b5b65c --- /dev/null +++ b/phitest/render/cuda/src/transformations.hpp @@ -0,0 +1,154 @@ +#pragma once + +#ifndef _INCLUDE_TRANFORMATIONS +#define _INCLUDE_TRANFORMATIONS + +#include "glm/mat4x4.hpp" +#include "glm/vec3.hpp" +#include "glm/vec4.hpp" +#include + +//#include "vectormath_helper.hpp" + +//namespace Transformations{ +#ifdef CBUF_FRUSTUM +struct FrustumParams{ + float near,far,left,right,top,bottom; +}; +__constant__ FrustumParams c_frustum; + +__host__ inline void setFrustumParams(FrustumParams &frustum, const float* params){ + //memcpy(&frustum, params, sizeof(FrustumParams)); + checkCudaErrors(cudaMemcpyToSymbol(c_frustum, params, sizeof(FrustumParams))); +} +#endif //CBUF_FRUSTUM + +using mat4 = glm::mat4; + +struct Transformations{ + mat4 M_model; + mat4 M_view; + mat4 M_modelView; + mat4 M_projection; +#ifdef CBUF_TRANSFORM_INVERSE + mat4 M_model_inv; + mat4 M_view_inv; + mat4 M_modelView_inv; + mat4 M_projection_inv; +#endif //CBUF_TRANSFORM_INVERSE +}; +__constant__ Transformations c_transform; + +inline glm::mat4 mat4FromArray(const float* arr){ + return glm::mat4(arr[0], arr[1], arr[2], arr[3], + arr[4], arr[5], arr[6], arr[7], + arr[8], arr[9], arr[10], arr[11], + arr[12], arr[13], arr[14], arr[15]); +} +__host__ inline void setTransformations(Transformations& transforms, const float* M, const float* V, const float* P){ + memset(&transforms, 0, sizeof(Transformations)); + transforms.M_model = (M==nullptr)? glm::mat4(1.f) : mat4FromArray(M); + transforms.M_view = (V==nullptr)? glm::mat4(1.f) : mat4FromArray(V); + transforms.M_projection = mat4FromArray(P); + transforms.M_modelView = transforms.M_view * transforms.M_model; + /* + LOG("Model: " << LOG_M44_COL(transforms.M_model) << std::endl); + LOG("View: " << LOG_M44_COL(transforms.M_view) << std::endl); + LOG("Proj: " << LOG_M44_COL(transforms.M_projection) << std::endl); + LOG("MV: " << LOG_M44_COL(transforms.M_modelView) << std::endl);//*/ + //transforms.M_modelView = transforms.M_model * transforms.M_view; +#ifdef CBUF_TRANSFORM_INVERSE + transforms.M_model_inv = glm::inverse(transforms.M_model); + transforms.M_view_inv = glm::inverse(transforms.M_view); + transforms.M_projection_inv = glm::inverse(transforms.M_projection); + transforms.M_modelView_inv = glm::inverse(transforms.M_modelView); +#endif + + checkCudaErrors(cudaMemcpyToSymbol(c_transform, &transforms, sizeof(Transformations))); +} + +/* --- TRANSFORMATIONS --- +* Coordinate Spaces: +* (globalIdx: integer array index in the camera grid, 0<=IDX +__device__ constexpr T indexToCoords(const T idx){ return idx + 0.5f;} +template +__device__ constexpr T coordsToIndex(const T coords){ return coords - 0.5f;} + +//--- Transform Forward --- + +//convert between view- and object-space given the (inverse) model-view matrix +__device__ inline glm::vec4 OStoVS(const glm::vec4 positionOS){ + return c_transform.M_modelView * positionOS; +} +//convert between NDC [-1,1] and view-space given an (inverse) projection matrix +__device__ inline glm::vec4 VStoNDC(const glm::vec4 positionVS){ + glm::vec4 positionNDC = c_transform.M_projection * positionVS; + return positionNDC/positionNDC.w; +} +//global 3D thread index to normalized device coordinates (NDC) given the 3D dimentions of the dispatch +//(z is in [-1,1] after perspective divide in forward pass, not in [0,1] as stored in the depth buffer) +__device__ inline glm::vec3 NDCtoIDX(const glm::vec4 ndc, const glm::vec3 dimensions){ + glm::vec3 position_normalized = glm::fma(glm::xyz(ndc), glm::vec3(0.5f), glm::vec3(0.5f)); + return position_normalized * dimensions; +} +//wrapper to convert thread index directly to object space + +__device__ inline glm::vec3 OStoIDX(const glm::vec4 positionOS, const glm::vec3 dimensions){ + return NDCtoIDX(VStoNDC(OStoVS(positionOS)), dimensions); +} +//change depth to be linear in VS for better sample coverage +__device__ inline glm::vec3 OStoIDXlinearDepth(const glm::vec4 positionOS, const glm::vec3 dimensions){ + glm::vec4 ndc = VStoNDC(OStoVS(positionOS)); + glm::vec3 idx = NDCtoIDX(ndc, dimensions); + + //correct mapping of ndc to idx s.t. idx has uniform depth/distance in VS + float z = -c_transform.M_projection[3].z/(ndc.z + c_transform.M_projection[2].z); + float t = -(z+c_frustum.near)/(c_frustum.far-c_frustum.near); //z=-(n+t*(f-n)) -> t = -(z+n)/(f-n) + idx.z = t*dimensions.z; + + return idx; + //*/ +} + +//--- Transform Backward +__device__ inline glm::vec4 IDXtoNDC(const glm::vec3 idx, const glm::vec3 dimensions_inv){ + const glm::vec3 position_normalized = idx * dimensions_inv; + return glm::vec4(glm::fma(position_normalized, glm::vec3(2.0f), glm::vec3(-1.0f)), 1.0f); +} +#ifdef CBUF_TRANSFORM_INVERSE +__device__ inline glm::vec4 NDCtoVS(const glm::vec4 positionNDC){ + glm::vec4 positionVS = c_transform.M_projection_inv * positionNDC; + return positionVS/positionVS.w; +} +__device__ inline glm::vec4 VStoOS(const glm::vec4 positionVS){ + return c_transform.M_modelView_inv * positionVS; +} +__device__ inline glm::vec4 IDXtoOS(const glm::vec3 idx, const glm::vec3 dimensions_inv){ + return VStoOS(NDCtoVS(IDXtoNDC(idx, dimensions_inv))); +} +__device__ inline glm::vec4 IDXtoOSlinearDepth(const glm::vec3 idx, const glm::vec3 dimensions_inv){ + glm::vec4 ndc = IDXtoNDC(idx, dimensions_inv); + + //correct mapping of idx to ndc s.t. idx has uniform depth/distance in VS + float t = idx.z * dimensions_inv.z; //float(idx.z) + float z = - vmath::flerp(c_frustum.near, c_frustum.far, t); + ndc.z = -c_transform.M_projection[2].z -c_transform.M_projection[3].z/z;//VStoNDC(glm::vec4(0.f,0.f,z,1.f)).z; // + + return VStoOS(NDCtoVS(ndc)); + //*/ +} +#endif //CBUF_TRANSFORM_INVERSE + +//} //namespace Transformations + +#endif //_INCLUDE_TRANFORMATIONS \ No newline at end of file diff --git a/phitest/render/cuda/src/transformations_v2.hpp b/phitest/render/cuda/src/transformations_v2.hpp new file mode 100644 index 0000000..896eab4 --- /dev/null +++ b/phitest/render/cuda/src/transformations_v2.hpp @@ -0,0 +1,165 @@ +#pragma once + +#ifndef _INCLUDE_TRANFORMATIONS_2 +#define _INCLUDE_TRANFORMATIONS_2 + +#include "vectormath.hpp" + +//namespace Transformations{ +#ifdef CBUF_FRUSTUM +struct FrustumParams{ + float near,far,left,right,top,bottom; +}; +__constant__ FrustumParams c_frustum; + +__host__ inline void setFrustumParams(FrustumParams &frustum, const float* params){ + //memcpy(&frustum, params, sizeof(FrustumParams)); + checkCudaErrors(cudaMemcpyToSymbol(c_frustum, params, sizeof(FrustumParams))); +} +#endif //CBUF_FRUSTUM + +using mat4 = vmath::float4x4; + +struct Transformations{ + mat4 M_model; + mat4 M_view; + mat4 M_modelView; + mat4 M_projection; +#ifdef CBUF_TRANSFORM_INVERSE + mat4 M_model_inv; + mat4 M_view_inv; + mat4 M_modelView_inv; + mat4 M_projection_inv; +#endif //CBUF_TRANSFORM_INVERSE +#ifdef CBUF_TRANSFORM_NORMAL + mat4 M_model_inv_T; +#endif //CBUF_TRANSFORM_NORMAL +}; +__constant__ Transformations c_transform; + +__host__ inline void setTransformations(Transformations& transforms, const float* M, const float* V, const float* P){ + memset(&transforms, 0, sizeof(Transformations)); + transforms.M_model = (M==nullptr)? vmath::make_float4x4(1.f) : vmath::make_float4x4(M); + transforms.M_view = (V==nullptr)? vmath::make_float4x4(1.f) : vmath::make_float4x4(V); + transforms.M_projection = vmath::make_float4x4(P); + transforms.M_modelView = vmath::matmul(transforms.M_view, transforms.M_model); + /* + LOG("Model: " << LOG_M44_COL(transforms.M_model) << std::endl); + LOG("View: " << LOG_M44_COL(transforms.M_view) << std::endl); + LOG("Proj: " << LOG_M44_COL(transforms.M_projection) << std::endl); + LOG("MV: " << LOG_M44_COL(transforms.M_modelView) << std::endl);//*/ + //transforms.M_modelView = transforms.M_model * transforms.M_view; +#ifdef CBUF_TRANSFORM_INVERSE + transforms.M_model_inv = vmath::inverse(transforms.M_model); + transforms.M_view_inv = vmath::inverse(transforms.M_view); + transforms.M_projection_inv = vmath::inverse(transforms.M_projection); + transforms.M_modelView_inv = vmath::inverse(transforms.M_modelView); +#endif +#ifdef CBUF_TRANSFORM_NORMAL + #ifdef CBUF_TRANSFORM_INVERSE + transforms.M_model_inv_T = vmath::transpose(transforms.M_model_inv); + #else + transforms.M_model_inv_T = vmath::transpose(vmath::inverse(transforms.M_model)); + #endif +#endif + + checkCudaErrors(cudaMemcpyToSymbol(c_transform, &transforms, sizeof(Transformations))); +} + +/* --- TRANSFORMATIONS --- +* Coordinate Spaces: +* (globalIdx: integer array index in the camera grid, 0<=IDX +__device__ constexpr T indexToCoords(const T idx){ return idx + 0.5f;} +template +__device__ constexpr T coordsToIndex(const T coords){ return coords - 0.5f;} + +// --- General Transform --- + +__device__ inline float4 TransformVector(const mat4 m, const float4 v){ + return vmath::matmul(m, v); +} +__device__ inline float3 TransformPosition(const mat4 m, const float3 v){ + return make_float3(TransformVector(m, make_float4(v, 1.0f))); +} +__device__ inline float3 TransformDirection(const mat4 m, const float3 v){ + return make_float3(TransformVector(m, make_float4(v, 0.0f))); +} + +//--- Transform Forward --- + +//convert between view- and object-space given the (inverse) model-view matrix +__device__ inline float4 OStoVS(const float4 positionOS){ + return vmath::matmul(c_transform.M_modelView, positionOS); +} +//convert between NDC [-1,1] and view-space given an (inverse) projection matrix +__device__ inline float4 VStoNDC(const float4 positionVS){ + float4 positionNDC = vmath::matmul(c_transform.M_projection, positionVS); + return positionNDC/positionNDC.w; +} +//global 3D thread index to normalized device coordinates (NDC) given the 3D dimentions of the dispatch +//(z is in [-1,1] after perspective divide in forward pass, not in [0,1] as stored in the depth buffer) +__device__ inline float3 NDCtoIDX(const float4 ndc, const float3 dimensions){ + float3 position_normalized = (make_float3(ndc) * 0.5f) + 0.5f; + return position_normalized * dimensions; +} +//wrapper to convert thread index directly to object space + +__device__ inline float3 OStoIDX(const float4 positionOS, const float3 dimensions){ + return NDCtoIDX(VStoNDC(OStoVS(positionOS)), dimensions); +} +//change depth to be linear in VS for better sample coverage +__device__ inline float3 OStoIDXlinearDepth(const float4 positionOS, const float3 dimensions){ + float4 ndc = VStoNDC(OStoVS(positionOS)); + float3 idx = NDCtoIDX(ndc, dimensions); + + //correct mapping of ndc to idx s.t. idx has uniform depth/distance in VS + float z = -c_transform.M_projection.c3.z/(ndc.z + c_transform.M_projection.c2.z); + float t = -(z+c_frustum.near)/(c_frustum.far-c_frustum.near); //z=-(n+t*(f-n)) -> t = -(z+n)/(f-n) + idx.z = t*dimensions.z; + + return idx; + //*/ +} + +//--- Transform Backward +__device__ inline float4 IDXtoNDC(const float3 idx, const float3 dimensions_inv){ + const float3 position_normalized = idx * dimensions_inv; + return make_float4((position_normalized * 2.0f) - 1.0f, 1.0f); +} +#ifdef CBUF_TRANSFORM_INVERSE +__device__ inline float4 NDCtoVS(const float4 positionNDC){ + float4 positionVS = vmath::matmul(c_transform.M_projection_inv, positionNDC); + return positionVS/positionVS.w; +} +__device__ inline float4 VStoOS(const float4 positionVS){ + return vmath::matmul(c_transform.M_modelView_inv, positionVS); +} +__device__ inline float4 IDXtoOS(const float3 idx, const float3 dimensions_inv){ + return VStoOS(NDCtoVS(IDXtoNDC(idx, dimensions_inv))); +} +__device__ inline float4 IDXtoOSlinearDepth(const float3 idx, const float3 dimensions_inv){ + float4 ndc = IDXtoNDC(idx, dimensions_inv); + + //correct mapping of idx to ndc s.t. idx has uniform depth/distance in VS + float t = idx.z * dimensions_inv.z; //float(idx.z) + float z = - lerp(c_frustum.near, c_frustum.far, t); + ndc.z = -c_transform.M_projection.c2.z -c_transform.M_projection.c3.z/z;//VStoNDC(float4(0.f,0.f,z,1.f)).z; // + + return VStoOS(NDCtoVS(ndc)); + //*/ +} +#endif //CBUF_TRANSFORM_INVERSE + +//} //namespace Transformations + +#endif //_INCLUDE_TRANFORMATIONS_2 \ No newline at end of file diff --git a/phitest/render/cuda/src/vector_io.hpp b/phitest/render/cuda/src/vector_io.hpp new file mode 100644 index 0000000..6e644e2 --- /dev/null +++ b/phitest/render/cuda/src/vector_io.hpp @@ -0,0 +1,75 @@ +#pragma once + +#ifndef VECTOR_IO +#define VECTOR_IO + +namespace vectorIO{ + + + template + __device__ constexpr size_t flatIdx4D(const C pos, const D dims); + template + __device__ constexpr size_t flatIdx3D(const C pos, const D dims); + template + __device__ constexpr size_t flatIdx3DChannel(const C pos, const I channel, const D dims, const I channel_dim); + template + __device__ constexpr size_t flatIdx2D(const C pos, const D dims); + + template + __device__ constexpr size_t flatIdx3D(const C x, const C y, const C z, const D dims); + template + __device__ constexpr size_t flatIdx2D(const C x, const C y, const D dims); + + template + __device__ constexpr C unflatIdx3D(const size_t flatIdx, const D dims); + + + //vector conversion cuda <-> glm + template + __device__ inline O toVector(const I v); + + //--- Buffer IO for Vector Types --- + //Read + template + __device__ inline V readVectorTypeAbs(const size_t idx, const T* buf); + + template + __device__ inline V readVectorType(const size_t idx, const T* buf); + + template + __device__ inline V readVectorType3D(const I pos, const I dims, const T* buf); + + template + __device__ inline V readVectorType3D(const C x, const C y, const C z, const I dims, const T* buf); + + template + __device__ inline V readVectorType3DBounds(const I pos, const I dims, const T* buf); + + template + __device__ inline V readVectorType3DBounds(const C x, const C y, const C z, const I dims, const T* buf); + + //Write + + //vector type V and single element type T with vector size VSIZE. T must be compatible with V. + //write the first VSIZE elements of vector 'v' to flat index 'idx'. idx is relative to T. + template + __device__ inline void writeVectorTypeAbs(const V v, const size_t idx, T* buf); + + template + __device__ inline void writeVectorType(const V v, const size_t idx, T* buf); + + template + __device__ inline void writeVectorType3D(const V v, const I pos, const I dims, T* buf); + + /* + * pass globalIdx to avoid recomputation + */ + /* reading properly aligned 1,2,4,8,16 byte structs is automatically coalesced, so no need for this + template + __device__ inline void writeVector4Type3DCoalescedX(const V data, const I globalIdx, const I dimensions, T* buf); + //*/ +} + +#include "vector_io.inl" + +#endif //VECTOR_IO \ No newline at end of file diff --git a/phitest/render/cuda/src/vector_io.inl b/phitest/render/cuda/src/vector_io.inl new file mode 100644 index 0000000..5a3d786 --- /dev/null +++ b/phitest/render/cuda/src/vector_io.inl @@ -0,0 +1,363 @@ + +//TODO specify more as needed... + +#define NUM_ELEMENTS(vector_type, element_type) (sizeof(vector_type)/sizeof(element_type)) + +#include "glm/vec2.hpp" +#include "glm/vec3.hpp" +#include "glm/vec4.hpp" + +namespace vectorIO{ + + inline int32_t idxMod(const int32_t x, const int32_t m){ + //int32_t r = x%m; + return x%m + (x<0 ? m : 0); + } + + template + __device__ constexpr size_t flatIdx4D(const C pos, const D dims){ + return dims.x*(dims.y*(dims.z*pos.w + pos.z) + pos.y) + pos.x; //dims.x*dims.y*pos.z + dims.x*pos.y + pos.x + } + template + __device__ constexpr size_t flatIdx3D(const C pos, const D dims){ + return dims.x*(dims.y*pos.z + pos.y) + pos.x; //dims.x*dims.y*pos.z + dims.x*pos.y + pos.x + } + template + __device__ constexpr size_t flatIdx3DChannel(const C pos, const I channel, const D dims, const I channel_dim){ + return channel_dim*(dims.x*(dims.y*pos.z + pos.y) + pos.x) + channel; + } + template + __device__ constexpr size_t flatIdx2D(const C pos, const D dims){ + return dims.x*pos.y + pos.x; + } + + template + __device__ constexpr size_t flatIdx3D(const C x, const C y, const C z, const D dims){ + return dims.x*(dims.y*z + y) + x; + } + template + __device__ constexpr size_t flatIdx2D(const C x, const C y, const D dims){ + return dims.x*y + x; + } + + template + __device__ constexpr C unflatIdx3D(const size_t flatIdx, const D dims){ + return C(flatIdx%dims.x, (flatIdx/dims.x)%dims.y, flatIdx/(dims.x*dims.y)); + } + + template<> + __device__ inline glm::vec4 toVector(const float4 v){ + return glm::vec4(v.x, v.y, v.z, v.w); + } + template<> + __device__ inline glm::vec3 toVector(const float4 v){ + return glm::vec3(v.x, v.y, v.z); + } + template<> + __device__ inline glm::vec3 toVector(const float3 v){ + return glm::vec3(v.x, v.y, v.z); + } + template<> + __device__ inline float4 toVector(const glm::vec4 v){ + return make_float4(v.x, v.y, v.z, v.w); + } + + //read + template + __device__ inline V readVectorTypeAbs(const size_t idx, const T* buf){ + V v; + #pragma unroll VSIZE + for(int32_t i=0;i + __device__ inline float4 readVectorTypeAbs(const size_t idx, const float4* buf){ + return buf[idx]; + } + template<> + __device__ inline glm::vec4 readVectorTypeAbs(const size_t idx, const float4* buf){ + return toVector(buf[idx]); + } + template<> + __device__ inline float3 readVectorTypeAbs(const size_t idx, const float3* buf){ + return buf[idx]; + } + template<> + __device__ inline float2 readVectorTypeAbs(const size_t idx, const float2* buf){ + return buf[idx]; + } + template<> + __device__ inline float1 readVectorTypeAbs(const size_t idx, const float1* buf){ + return buf[idx]; + } + template<> + __device__ inline float readVectorTypeAbs(const size_t idx, const float* buf){ + return buf[idx]; + } + + template + __device__ inline V readVectorType(const size_t idx, const T* buf){ + return readVectorTypeAbs(idx*NUM_ELEMENTS(V,T), buf); + } + template + __device__ inline V readVectorType3D(const I pos, const I dims, const T* buf){ + return readVectorType(flatIdx3D(pos, dims), buf); + } + template + __device__ inline V readVectorType3D(const C x, const C y, const C z, const I dims, const T* buf){ + return readVectorType(flatIdx3D(x,y,z, dims), buf); + } + //with bounds checking + template + __device__ inline V readVectorType3DBounds(const I pos, const I dims, const T* buf){ + if(0(pos,dims,buf); + }else{ + return vmath::make_cudaFloat(0.f); + } + } + template + __device__ inline V readVectorType3DBounds(const C x, const C y, const C z, const I dims, const T* buf){ + if(0(x,y,z,dims,buf); + }else{ + return vmath::make_cudaFloat(0.f); + } + } + + + //write + template + __device__ inline void writeVectorTypeAbs(const V v, const size_t idx, T* buf){ + #pragma unroll VSIZE + for(int32_t i=0;i + __device__ inline void writeVectorTypeAbs(const float4 v, const size_t idx, float4* buf){ + buf[idx] = v; + } + template<> + __device__ inline void writeVectorTypeAbs(const glm::vec4 v, const size_t idx, float4* buf){ + buf[idx] = toVector(v); + } + template<> + __device__ inline void writeVectorTypeAbs(const float4 v, const size_t idx, float* buf){ + buf[idx] = v.x; + buf[idx+1] = v.y; + buf[idx+2] = v.z; + buf[idx+3] = v.w; + } + template<> + __device__ inline void writeVectorTypeAbs(const float2 v, const size_t idx, float2* buf){ + buf[idx] = v; + } + template<> + __device__ inline void writeVectorTypeAbs(const float1 v, const size_t idx, float1* buf){ + buf[idx] = v; + } + template<> + __device__ inline void writeVectorTypeAbs(const float1 v, const size_t idx, float* buf){ + buf[idx] = v.x; + } + template<> + __device__ inline void writeVectorTypeAbs(const float v, const size_t idx, float* buf){ + buf[idx] = v; + } + + template + __device__ inline void writeVectorType(const V v, const size_t idx, T* buf){ + writeVectorTypeAbs(v, idx*NUM_ELEMENTS(V,T), buf); + } + template + __device__ inline void writeVectorType3D(const V v, const I pos, const I dims, T* buf){ + writeVectorType(v, flatIdx3D(pos, dims), buf); + } + +// add + + template + __device__ inline void addVectorTypeAbs(const V v, const size_t idx, T* buf){ + #pragma unroll VSIZE + for(int32_t i=0;i + __device__ inline void addVectorTypeAbs(const float4 v, const size_t idx, float4* buf){ + buf[idx] += v; + } + template<> + __device__ inline void addVectorTypeAbs(const glm::vec4 v, const size_t idx, float4* buf){ + buf[idx] += toVector(v); + } + template<> + __device__ inline void addVectorTypeAbs(const float4 v, const size_t idx, float* buf){ + buf[idx] += v.x; + buf[idx+1] += v.y; + buf[idx+2] += v.z; + buf[idx+3] += v.w; + } + template<> + __device__ inline void addVectorTypeAbs(const float2 v, const size_t idx, float2* buf){ + buf[idx] += v; + } + template<> + __device__ inline void addVectorTypeAbs(const float1 v, const size_t idx, float1* buf){ + buf[idx] += v; + } + template<> + __device__ inline void addVectorTypeAbs(const float1 v, const size_t idx, float* buf){ + buf[idx] += v.x; + } + template<> + __device__ inline void addVectorTypeAbs(const float v, const size_t idx, float* buf){ + buf[idx] += v; + } + template + __device__ inline void addVectorType(const V v, const size_t idx, T* buf){ + addVectorTypeAbs(v, idx*NUM_ELEMENTS(V,T), buf); + } + template + __device__ inline void addVectorType3D(const V v, const I pos, const I dims, T* buf){ + addVectorType(v, flatIdx3D(pos, dims), buf); + } + +// atomic add + + template + __device__ inline void atomicAddVectorTypeAbs(const T v, const size_t idx, T* buf); + template<> + __device__ inline void atomicAddVectorTypeAbs(const float1 v, const size_t idx, float1* buf){ + float * buf_raw = reinterpret_cast(buf + idx); + atomicAdd(buf_raw, v.x); + } + template<> + __device__ inline void atomicAddVectorTypeAbs(const float2 v, const size_t idx, float2* buf){ + float * buf_raw = reinterpret_cast(buf + idx); + atomicAdd(buf_raw, v.x); + atomicAdd(buf_raw +1, v.y); + } + template<> + __device__ inline void atomicAddVectorTypeAbs(const float4 v, const size_t idx, float4* buf){ + float * buf_raw = reinterpret_cast(buf + idx); + atomicAdd(buf_raw, v.x); + atomicAdd(buf_raw +1, v.y); + atomicAdd(buf_raw +2, v.z); + atomicAdd(buf_raw +3, v.w); + } + + template + __device__ inline void atomicAddVectorType3D(const T v, const int3 pos, const int3 dims, T* buf); + template<> + __device__ inline void atomicAddVectorType3D(const float1 v, const int3 pos, const int3 dims, float1* buf){ + const size_t offset = flatIdx3D(pos, dims); + float * buf_raw = reinterpret_cast(buf + offset); + atomicAdd(buf_raw, v.x); + } + template<> + __device__ inline void atomicAddVectorType3D(const float2 v, const int3 pos, const int3 dims, float2* buf){ + const size_t offset = flatIdx3D(pos, dims); + float * buf_raw = reinterpret_cast(buf + offset); + atomicAdd(buf_raw, v.x); + atomicAdd(buf_raw +1, v.y); + } + template<> + __device__ inline void atomicAddVectorType3D(const float4 v, const int3 pos, const int3 dims, float4* buf){ + const size_t offset = flatIdx3D(pos, dims); + float * buf_raw = reinterpret_cast(buf + offset); + atomicAdd(buf_raw, v.x); + atomicAdd(buf_raw +1, v.y); + atomicAdd(buf_raw +2, v.z); + atomicAdd(buf_raw +3, v.w); + } + template + __device__ inline void atomicAddVectorType3D(const T v, const int32_t x, const int32_t y, const int32_t z, const int3 dims, T* buf); + template<> + __device__ inline void atomicAddVectorType3D(const float1 v, const int32_t x, const int32_t y, const int32_t z, const int3 dims, float1* buf){ + const size_t offset = flatIdx3D(x,y,z, dims); + float * buf_raw = reinterpret_cast(buf + offset); + atomicAdd(buf_raw, v.x); + } + template<> + __device__ inline void atomicAddVectorType3D(const float2 v, const int32_t x, const int32_t y, const int32_t z, const int3 dims, float2* buf){ + const size_t offset = flatIdx3D(x,y,z, dims); + float * buf_raw = reinterpret_cast(buf + offset); + atomicAdd(buf_raw, v.x); + atomicAdd(buf_raw +1, v.y); + } + template<> + __device__ inline void atomicAddVectorType3D(const float4 v, const int32_t x, const int32_t y, const int32_t z, const int3 dims, float4* buf){ + const size_t offset = flatIdx3D(x,y,z, dims); + float * buf_raw = reinterpret_cast(buf + offset); + atomicAdd(buf_raw, v.x); + atomicAdd(buf_raw +1, v.y); + atomicAdd(buf_raw +2, v.z); + atomicAdd(buf_raw +3, v.w); + } + + //performance test + /* + * pass globalIdx to avoid recomputation + */ + /* reading properly aligned 1,2,4,8,16 byte structs is automatically coalesced, so no need for this + template + __device__ inline void writeVector4Type3DCoalescedX(const V data, const I globalIdx, const I dimensions, T* buf){ + __shared__ T s_vectorBuffer[BLOCK_SIZE_Z*BLOCK_SIZE_Y][BLOCK_SIZE_X*4+1]; //+1 for better bank access? + const int32_t blockThreadIdxZY = BLOCK_SIZE_Y*threadIdx.z + threadIdx.y; + const int32_t blockThreadIdxX = threadIdx.x*4; + //#pragma unroll NUM_ELEMENTS(V,T) + s_vectorBuffer[blockThreadIdxZY][blockThreadIdxX] = data.x; + s_vectorBuffer[blockThreadIdxZY][blockThreadIdxX+1] = data.y; + s_vectorBuffer[blockThreadIdxZY][blockThreadIdxX+2] = data.z; + s_vectorBuffer[blockThreadIdxZY][blockThreadIdxX+3] = data.w; + //start offset in global row in vectors (V) + const int32_t offsetX = blockIdx.x*BLOCK_SIZE_X; + //start of row block in global memory in elements (T) + const int32_t globalIdxZYx = 4* (offsetX + dimensions.x*(dimensions.y*globalIdx.z + globalIdx.y)); + //stride is BLOCK_SIZE_X, should be at least 8 when writing 4byte components + __syncthreads(); + #pragma unroll + for(int32_t i=0;i<4;++i){ + const int32_t currentThreadOffset = BLOCK_SIZE_X*i + threadIdx.x; + //clamp bounds + if(globalIdx.y + __device__ inline void writeVector4Type3DCoalescedXShuffle(const V data, const I globalIdx, const I dimensions, T* buf){ + + int32_t shuffleIdx=threadIdx.x%4; + T shuffle_data = {data.x, data.y, data.z, data.w}; + //shuffle_data[shuffleIdx] = data[shuffle_idx]; + for(int32_t i=1;i<4;++i){ + shuffle_data[(shuffleIdx+i)%4] = __shfl_down_sync(0xffffffff, data[idxMod(shuffleIdx-i, 4)], i, 4); + } + //start offset in global row in vectors (V) + const int32_t offsetX = blockIdx.x*BLOCK_SIZE_X; + //start of row block in global memory in elements (T) + const int32_t globalIdxZYx = 4* (offsetX + dimensions.x*(dimensions.y*globalIdx.z + globalIdx.y)); + //stride is BLOCK_SIZE_X, should be at least 8 when writing 4byte components + #pragma unroll + for(int32_t i=0;i<4;++i){ + const int32_t currentThreadOffset = BLOCK_SIZE_X*i + threadIdx.x; + //clamp bounds + if(globalIdx.y + __device__ inline void writeVector4Type3DCoalescedX(const float1 data, const glm::ivec3 globalIdx, const glm::ivec3 dimensions, float* buf){ + writeVectorType3D(data, globalIdx, dimensions, buf); + }//*/ +} \ No newline at end of file diff --git a/phitest/render/cuda/src/vectormath.hpp b/phitest/render/cuda/src/vectormath.hpp new file mode 100644 index 0000000..c0e0e1d --- /dev/null +++ b/phitest/render/cuda/src/vectormath.hpp @@ -0,0 +1,613 @@ +#pragma once + +#ifndef _INCLUDE_VECTORMATH +#define _INCLUDE_VECTORMATH + +//#include "cuda_runtime.h" +#include "cuda-samples/Common/helper_math.h" //math functions for cuda vector types, from cuda samples + +#ifndef FLOAT_MIN +#define FLOAT_MIN 1.17549e-38 +#endif + +#ifndef FLOAT_MAX +#define FLOAT_MAX 3.40282e+38 +#endif + +#ifndef FLOAT_LOWEST +#define FLOAT_LOWEST - FLOAT_MAX +#endif + +/* Extensions and missing functions of helper_math*/ +inline __host__ __device__ float1 make_float1(int1 a) +{ + return make_float1(float(a.x)); +} +inline __host__ __device__ float1 make_float1(uint1 a) +{ + return make_float1(float(a.x)); +} + +inline __host__ __device__ float3 operator<(const float3 v, const float b) +{ + return make_float3(v.x +__device__ __host__ inline T make_cudaFloat(const A a); +template<> +__device__ __host__ inline float1 make_cudaFloat(const float a){return make_float1(a);} +template<> +__device__ __host__ inline float2 make_cudaFloat(const float a){return make_float2(a);} +template<> +__device__ __host__ inline float3 make_cudaFloat(const float a){return make_float3(a);} +template<> +__device__ __host__ inline float4 make_cudaFloat(const float a){return make_float4(a);} +template<> +__device__ __host__ inline float1 make_cudaFloat(const int1 a){return make_float1(a);} +template<> +__device__ __host__ inline float2 make_cudaFloat(const int2 a){return make_float2(a);} +template<> +__device__ __host__ inline float3 make_cudaFloat(const int3 a){return make_float3(a);} +template<> +__device__ __host__ inline float4 make_cudaFloat(const int4 a){return make_float4(a);} + +/*Vector functions*/ + +constexpr __host__ __device__ float sum(const float1 v){ + return v.x; +} +constexpr __host__ __device__ size_t sum(const int1 v){ + return v.x; +} +constexpr __host__ __device__ float sum(const float2 v){ + return v.x + v.y; +} +constexpr __host__ __device__ size_t sum(const int2 v){ + return v.x + v.y; +} +constexpr __host__ __device__ float sum(const float3 v){ + return v.x + v.y + v.z; +} +constexpr __host__ __device__ size_t sum(const int3 v){ + return v.x + v.y + v.z; +} +constexpr __host__ __device__ float sum(const float4 v){ + return v.x + v.y + v.z + v.w; +} +constexpr __host__ __device__ size_t sum(const int4 v){ + return v.x + v.y + v.z + v.w; +} + +constexpr __host__ __device__ float prod(const float1 v){ + return v.x; +} +constexpr __host__ __device__ size_t prod(const int1 v){ + return v.x; +} +constexpr __host__ __device__ float prod(const float2 v){ + return v.x * v.y; +} +constexpr __host__ __device__ size_t prod(const int2 v){ + return v.x * v.y; +} +constexpr __host__ __device__ float prod(const float3 v){ + return v.x * v.y * v.z; +} +constexpr __host__ __device__ size_t prod(const int3 v){ + return v.x * v.y * v.z; +} +constexpr __host__ __device__ float prod(const float4 v){ + return v.x * v.y * v.z * v.w; +} +constexpr __host__ __device__ size_t prod(const int4 v){ + return v.x * v.y * v.z * v.w; +} + +inline __host__ __device__ float step(const float x, const float y){ + return(float)x +inline __device__ __host__ T lerp(T a, T b, T t) +{ + return a + t*(b-a); +} + +//https://stackoverflow.com/questions/14997165/fastest-way-to-get-a-positive-modulo-in-c-c +template +constexpr __host__ __device__ T positivemod(const T v, const M m){ +// T mod = v%m; +// T isNegative = v<0; +// return mod + m*isNegative; + return (v%m) + m*(v<0); +} + +inline __host__ __device__ int3 pmod(const int3 v, const int3 m){ + return make_int3( + (v.x%m.x) + m.x*(v.x<0), + (v.y%m.y) + m.y*(v.y<0), + (v.z%m.z) + m.z*(v.z<0) + ); +} +inline __host__ __device__ int3 pmod(const int3 v, const int32_t m){ + return make_int3( + (v.x%m) + m*(v.x<0), + (v.y%m) + m*(v.y<0), + (v.z%m) + m*(v.z<0) + ); +} + + +/* Matix types, column-major*/ +typedef struct float3x3{ + float3 c0,c1,c2; +} float3x3; + +typedef struct __align__(16) float4x4{ + float4 c0,c1,c2,c3; +} float4x4; + +inline __host__ __device__ float4x4 make_float4x4( + const float m00, const float m01, const float m02, const float m03, + const float m10, const float m11, const float m12, const float m13, + const float m20, const float m21, const float m22, const float m23, + const float m30, const float m31, const float m32, const float m33){ + return (float4x4){ + .c0 = make_float4(m00, m01, m02, m03), + .c1 = make_float4(m10, m11, m12, m13), + .c2 = make_float4(m20, m21, m22, m23), + .c3 = make_float4(m30, m31, m32, m33) + }; +} +inline __host__ __device__ float4x4 make_float4x4(const float *arr){ + return make_float4x4( + arr[0], arr[1], arr[2], arr[3], + arr[4], arr[5], arr[6], arr[7], + arr[8], arr[9], arr[10], arr[11], + arr[12], arr[13], arr[14], arr[15] + ); +} +inline __host__ __device__ float4x4 make_float4x4(const float4 c0,const float4 c1, const float4 c2, const float4 c3){ + return (float4x4){.c0 = c0,.c1 = c1, .c2 = c2, .c3 = c3}; +} +inline __host__ __device__ float4x4 make_float4x4(const float4 diag){ + return make_float4x4( + diag.x, 0.f, 0.f, 0.f, + 0.f, diag.y, 0.f, 0.f, + 0.f, 0.f, diag.z, 0.f, + 0.f, 0.f, 0.f, diag.w + ); +} +inline __host__ __device__ float4x4 make_float4x4(const float diag){ + return make_float4x4( + diag, 0.f, 0.f, 0.f, + 0.f, diag, 0.f, 0.f, + 0.f, 0.f, diag, 0.f, + 0.f, 0.f, 0.f, diag + ); +} + +/* Matix functions*/ +inline __host__ __device__ float4x4 transpose(const float4x4 m){ + return make_float4x4( + m.c0.x, m.c1.x, m.c2.x, m.c3.x, + m.c0.y, m.c1.y, m.c2.y, m.c3.y, + m.c0.z, m.c1.z, m.c2.z, m.c3.z, + m.c0.w, m.c1.w, m.c2.w, m.c3.w + ); +} + +inline __host__ __device__ float4x4 operator*(const float4x4 m, const float s){ + return make_float4x4( + m.c0 * s, + m.c1 * s, + m.c2 * s, + m.c3 * s + ); +} + +inline __host__ float4x4 inverse(const float4x4 m){ + //from GLM + float Coef00 = m.c2.z * m.c3.w - m.c3.z * m.c2.w; //m[2][2] * m[3][3] - m[3][2] * m[2][3]; + float Coef02 = m.c1.z * m.c3.w - m.c3.z * m.c1.w; //m[1][2] * m[3][3] - m[3][2] * m[1][3]; + float Coef03 = m.c1.z * m.c2.w - m.c2.z * m.c1.w; //m[1][2] * m[2][3] - m[2][2] * m[1][3]; + + float Coef04 = m.c2.y * m.c3.w - m.c3.y * m.c2.w; //m[2][1] * m[3][3] - m[3][1] * m[2][3]; + float Coef06 = m.c1.y * m.c3.w - m.c3.y * m.c1.w; //m[1][1] * m[3][3] - m[3][1] * m[1][3]; + float Coef07 = m.c1.y * m.c2.w - m.c2.y * m.c1.w; //m[1][1] * m[2][3] - m[2][1] * m[1][3]; + + float Coef08 = m.c2.y * m.c3.z - m.c3.y * m.c2.z; //m[2][1] * m[3][2] - m[3][1] * m[2][2]; + float Coef10 = m.c1.y * m.c3.z - m.c3.y * m.c1.z; //m[1][1] * m[3][2] - m[3][1] * m[1][2]; + float Coef11 = m.c1.y * m.c2.z - m.c2.y * m.c1.z; //m[1][1] * m[2][2] - m[2][1] * m[1][2]; + + float Coef12 = m.c2.x * m.c3.w - m.c3.x * m.c2.w; //m[2][0] * m[3][3] - m[3][0] * m[2][3]; + float Coef14 = m.c1.x * m.c3.w - m.c3.x * m.c1.w; //m[1][0] * m[3][3] - m[3][0] * m[1][3]; + float Coef15 = m.c1.x * m.c2.w - m.c2.x * m.c1.w; //m[1][0] * m[2][3] - m[2][0] * m[1][3]; + + float Coef16 = m.c2.x * m.c3.z - m.c3.x * m.c2.z; //m[2][0] * m[3][2] - m[3][0] * m[2][2]; + float Coef18 = m.c1.x * m.c3.z - m.c3.x * m.c1.z; //m[1][0] * m[3][2] - m[3][0] * m[1][2]; + float Coef19 = m.c1.x * m.c2.z - m.c2.x * m.c1.z; //m[1][0] * m[2][2] - m[2][0] * m[1][2]; + + float Coef20 = m.c2.x * m.c3.y - m.c3.x * m.c2.y; //m[2][0] * m[3][1] - m[3][0] * m[2][1]; + float Coef22 = m.c1.x * m.c3.y - m.c3.x * m.c1.y; //m[1][0] * m[3][1] - m[3][0] * m[1][1]; + float Coef23 = m.c1.x * m.c2.y - m.c2.x * m.c1.y; //m[1][0] * m[2][1] - m[2][0] * m[1][1]; + + float4 Fac0 = make_float4(Coef00, Coef00, Coef02, Coef03); + float4 Fac1 = make_float4(Coef04, Coef04, Coef06, Coef07); + float4 Fac2 = make_float4(Coef08, Coef08, Coef10, Coef11); + float4 Fac3 = make_float4(Coef12, Coef12, Coef14, Coef15); + float4 Fac4 = make_float4(Coef16, Coef16, Coef18, Coef19); + float4 Fac5 = make_float4(Coef20, Coef20, Coef22, Coef23); + + float4 Vec0 = make_float4(m.c1.x, m.c0.x, m.c0.x, m.c0.x); //m[1][0], m[0][0], m[0][0], m[0][0]); + float4 Vec1 = make_float4(m.c1.y, m.c0.y, m.c0.y, m.c0.y); //m[1][1], m[0][1], m[0][1], m[0][1]); + float4 Vec2 = make_float4(m.c1.z, m.c0.z, m.c0.z, m.c0.z); //m[1][2], m[0][2], m[0][2], m[0][2]); + float4 Vec3 = make_float4(m.c1.w, m.c0.w, m.c0.w, m.c0.w); //m[1][3], m[0][3], m[0][3], m[0][3]); + + float4 Inv0 = Vec1 * Fac0 - Vec2 * Fac1 + Vec3 * Fac2; + float4 Inv1 = Vec0 * Fac0 - Vec2 * Fac3 + Vec3 * Fac4; + float4 Inv2 = Vec0 * Fac1 - Vec1 * Fac3 + Vec3 * Fac5; + float4 Inv3 = Vec0 * Fac2 - Vec1 * Fac4 + Vec2 * Fac5; + + float4 SignA = make_float4(+1, -1, +1, -1); + float4 SignB = make_float4(-1, +1, -1, +1); + float4x4 Inverse = make_float4x4(Inv0 * SignA, Inv1 * SignB, Inv2 * SignA, Inv3 * SignB); + + float4 Row0 = make_float4(Inverse.c0.x, Inverse.c1.x, Inverse.c2.x, Inverse.c3.x); + + float Dot1 = dot(m.c0, Row0); //(m[0] * Row0); + //T Dot1 = (Dot0.x + Dot0.y) + (Dot0.z + Dot0.w); + + float OneOverDeterminant = 1.f / Dot1; + + return Inverse * OneOverDeterminant; +} + + +inline __host__ __device__ float4 matmul(const float4x4 m, const float4 v){ + return m.c0*v.x + m.c1*v.y + m.c2*v.z + m.c3*v.w; +} +inline __host__ __device__ float4 matmul(const float4 v, const float4x4 m){ + return make_float4(dot(m.c0, v), dot(m.c1, v), dot(m.c2, v), dot(m.c3, v)); +} +inline __host__ __device__ float4x4 matmul(const float4x4 m1, const float4x4 m2){ + return make_float4x4( + matmul(m1, m2.c0), + matmul(m1, m2.c1), + matmul(m1, m2.c2), + matmul(m1, m2.c3) + ); +} + +/* Vector swizzle */ +/* +inline __host__ __device__ float2 xx(const float1 v){ return make_float2(v.x, v.x);} +inline __host__ __device__ float2 xx(const float2 v){ return make_float2(v.x, v.x);} +inline __host__ __device__ float2 xx(const float3 v){ return make_float2(v.x, v.x);} +inline __host__ __device__ float2 xx(const float4 v){ return make_float2(v.x, v.x);} +*/ +#define SW2(v,a,b, type) make_##type##2(v.a, v.b) +#define SW2_F(v,a,b) SW2(v,a,b,float) +#define SW3(v,a,b,c, type) make_##type##3(v.a, v.b, v.c) +#define SW3_F(v,a,b,c) SW3(v,a,b,c,float) +#define SW4(v,a,b,c,d, type) make_##type##4(v.a, v.b, v.c, v.d) +#define SW4_F(v,a,b,c,d) SW4(v,a,b,c,d,float) + + +#define SWIZZLE2(a,b,T,type) inline __host__ __device__ type##2 a##b(const T v){ return make_##type##2(v.a, v.b);} +#define SWIZZLE3(a,b,c,T,type) inline __host__ __device__ type##3 a##b##c(const T v){ return make_##type##3(v.a, v.b, v.c);} +#define SWIZZLE4(a,b,c,d,T,type) inline __host__ __device__ type##4 a##b##c##d(const T v){ return make_##type##4(v.a, v.b, v.c, v.d);} + +#define SWIZZLE2_TYPE(T) \ + SWIZZLE2(x,x,T##1,T) \ + SWIZZLE2(x,x,T##2,T) \ + SWIZZLE2(x,x,T##3,T) \ + SWIZZLE2(x,x,T##4,T) \ + SWIZZLE2(x,y,T##2,T) \ + SWIZZLE2(x,y,T##3,T) \ + SWIZZLE2(x,y,T##4,T) \ + SWIZZLE2(x,z,T##3,T) \ + SWIZZLE2(x,z,T##4,T) \ + SWIZZLE2(x,w,T##4,T) \ + \ + SWIZZLE2(y,x,T##2,T) \ + SWIZZLE2(y,x,T##3,T) \ + SWIZZLE2(y,x,T##4,T) \ + SWIZZLE2(y,y,T##2,T) \ + SWIZZLE2(y,y,T##3,T) \ + SWIZZLE2(y,y,T##4,T) \ + SWIZZLE2(y,z,T##3,T) \ + SWIZZLE2(y,z,T##4,T) \ + SWIZZLE2(y,w,T##4,T) \ + \ + SWIZZLE2(z,x,T##3,T) \ + SWIZZLE2(z,x,T##4,T) \ + SWIZZLE2(z,y,T##3,T) \ + SWIZZLE2(z,y,T##4,T) \ + SWIZZLE2(z,z,T##3,T) \ + SWIZZLE2(z,z,T##4,T) \ + SWIZZLE2(z,w,T##4,T) \ + \ + SWIZZLE2(w,x,T##4,T) \ + SWIZZLE2(w,y,T##4,T) \ + SWIZZLE2(w,z,T##4,T) \ + SWIZZLE2(w,w,T##4,T) + +#define SWIZZLE3_TYPE(T) \ + SWIZZLE3(x,x,x,T##1,T) \ + SWIZZLE3(x,x,x,T##2,T) \ + SWIZZLE3(x,x,x,T##3,T) \ + SWIZZLE3(x,x,x,T##4,T) \ + SWIZZLE3(x,x,y,T##2,T) \ + SWIZZLE3(x,x,y,T##3,T) \ + SWIZZLE3(x,x,y,T##4,T) \ + SWIZZLE3(x,x,z,T##3,T) \ + SWIZZLE3(x,x,z,T##4,T) \ + SWIZZLE3(x,x,w,T##4,T) \ + \ + SWIZZLE3(x,y,x,T##2,T) \ + SWIZZLE3(x,y,x,T##3,T) \ + SWIZZLE3(x,y,x,T##4,T) \ + SWIZZLE3(x,y,y,T##2,T) \ + SWIZZLE3(x,y,y,T##3,T) \ + SWIZZLE3(x,y,y,T##4,T) \ + SWIZZLE3(x,y,z,T##3,T) \ + SWIZZLE3(x,y,z,T##4,T) \ + SWIZZLE3(x,y,w,T##4,T) \ + \ + SWIZZLE3(x,z,x,T##3,T) \ + SWIZZLE3(x,z,x,T##4,T) \ + SWIZZLE3(x,z,y,T##3,T) \ + SWIZZLE3(x,z,y,T##4,T) \ + SWIZZLE3(x,z,z,T##3,T) \ + SWIZZLE3(x,z,z,T##4,T) \ + SWIZZLE3(x,z,w,T##4,T) \ + \ + SWIZZLE3(x,w,x,T##4,T) \ + SWIZZLE3(x,w,y,T##4,T) \ + SWIZZLE3(x,w,z,T##4,T) \ + SWIZZLE3(x,w,w,T##4,T) \ + \ + \ + SWIZZLE3(y,x,y,T##2,T) \ + SWIZZLE3(y,x,y,T##3,T) \ + SWIZZLE3(y,x,y,T##4,T) \ + SWIZZLE3(y,x,z,T##3,T) \ + SWIZZLE3(y,x,z,T##4,T) \ + SWIZZLE3(y,x,w,T##4,T) \ + \ + SWIZZLE3(y,y,x,T##2,T) \ + SWIZZLE3(y,y,x,T##3,T) \ + SWIZZLE3(y,y,x,T##4,T) \ + SWIZZLE3(y,y,y,T##2,T) \ + SWIZZLE3(y,y,y,T##3,T) \ + SWIZZLE3(y,y,y,T##4,T) \ + SWIZZLE3(y,y,z,T##3,T) \ + SWIZZLE3(y,y,z,T##4,T) \ + SWIZZLE3(y,y,w,T##4,T) \ + \ + SWIZZLE3(y,z,x,T##3,T) \ + SWIZZLE3(y,z,x,T##4,T) \ + SWIZZLE3(y,z,y,T##3,T) \ + SWIZZLE3(y,z,y,T##4,T) \ + SWIZZLE3(y,z,z,T##3,T) \ + SWIZZLE3(y,z,z,T##4,T) \ + SWIZZLE3(y,z,w,T##4,T) \ + \ + SWIZZLE3(y,w,x,T##4,T) \ + SWIZZLE3(y,w,y,T##4,T) \ + SWIZZLE3(y,w,z,T##4,T) \ + SWIZZLE3(y,w,w,T##4,T) \ + \ + \ + SWIZZLE3(z,x,y,T##3,T) \ + SWIZZLE3(z,x,y,T##4,T) \ + SWIZZLE3(z,x,z,T##3,T) \ + SWIZZLE3(z,x,z,T##4,T) \ + SWIZZLE3(z,x,w,T##4,T) \ + \ + SWIZZLE3(z,y,x,T##3,T) \ + SWIZZLE3(z,y,x,T##4,T) \ + SWIZZLE3(z,y,y,T##3,T) \ + SWIZZLE3(z,y,y,T##4,T) \ + SWIZZLE3(z,y,z,T##3,T) \ + SWIZZLE3(z,y,z,T##4,T) \ + SWIZZLE3(z,y,w,T##4,T) \ + \ + SWIZZLE3(z,z,x,T##3,T) \ + SWIZZLE3(z,z,x,T##4,T) \ + SWIZZLE3(z,z,y,T##3,T) \ + SWIZZLE3(z,z,y,T##4,T) \ + SWIZZLE3(z,z,z,T##3,T) \ + SWIZZLE3(z,z,z,T##4,T) \ + SWIZZLE3(z,z,w,T##4,T) \ + \ + SWIZZLE3(z,w,x,T##4,T) \ + SWIZZLE3(z,w,y,T##4,T) \ + SWIZZLE3(z,w,z,T##4,T) \ + SWIZZLE3(z,w,w,T##4,T) \ + \ + \ + SWIZZLE3(w,x,y,T##4,T) \ + SWIZZLE3(w,x,z,T##4,T) \ + SWIZZLE3(w,x,w,T##4,T) \ + \ + SWIZZLE3(w,y,x,T##4,T) \ + SWIZZLE3(w,y,y,T##4,T) \ + SWIZZLE3(w,y,z,T##4,T) \ + SWIZZLE3(w,y,w,T##4,T) \ + \ + SWIZZLE3(w,z,x,T##4,T) \ + SWIZZLE3(w,z,y,T##4,T) \ + SWIZZLE3(w,z,z,T##4,T) \ + SWIZZLE3(w,z,w,T##4,T) \ + \ + SWIZZLE3(w,w,x,T##4,T) \ + SWIZZLE3(w,w,y,T##4,T) \ + SWIZZLE3(w,w,z,T##4,T) \ + SWIZZLE3(w,w,w,T##4,T) + +/* +#define SWIZZLE4_TYPE(T) \ + SWIZZLE4(x,x,x,x,T##1,T) \ + SWIZZLE4(x,x,x,x,T##2,T) \ + SWIZZLE4(x,x,x,x,T##3,T) \ + SWIZZLE4(x,x,x,x,T##4,T) +... +*/ + +#undef SWIZZLE2_TYPE +#undef SWIZZLE3_TYPE + +#undef SWIZZLE2 +#undef SWIZZLE3 +#undef SWIZZLE4 + +} //END vmath + +#endif //_INCLUDE_VECTORMATH \ No newline at end of file diff --git a/phitest/render/cuda/src/vectormath_helper.hpp b/phitest/render/cuda/src/vectormath_helper.hpp new file mode 100644 index 0000000..a79fa2a --- /dev/null +++ b/phitest/render/cuda/src/vectormath_helper.hpp @@ -0,0 +1,304 @@ +#pragma once + +#ifndef VMATH_HELPER +#define VMATH_HELPER + +#define EXPAND_VECTOR_XYZ(v) v.x, v.y, v.z +#define EXPAND_VECTOR_YZW(v) v.y, v.z, v.w +#define EXPAND_VECTOR_ZYX(v) v.z, v.y, v.x + +#include "cuda-samples/Common/helper_math.h" //operators for cuda vector types + +#ifndef FLOAT_MIN +#define FLOAT_MIN 1.17549e-38 +#endif + +#ifndef FLOAT_MAX +#define FLOAT_MAX 3.40282e+38 +#endif + +#ifndef FLOAT_LOWEST +#define FLOAT_LOWEST - FLOAT_MAX +#endif + +//--- Missing cuda math helpers --- + +inline __host__ __device__ float1 operator+(const float1 a, const float1 b) +{ + return make_float1(a.x + b.x); +} +inline __host__ __device__ float1 operator-(const float1 a, const float1 b) +{ + return make_float1(a.x - b.x); +} +inline __host__ __device__ float1 operator*(const float1 a, const float1 b) +{ + return make_float1(a.x * b.x); +} +inline __host__ __device__ float1 operator*(const float1 a, const float b) +{ + return make_float1(a.x * b); +} +inline __host__ __device__ float1 operator*(const float a, const float1 b) +{ + return make_float1(a * b.x); +} +inline __host__ __device__ void operator+=(float1 &a, const float1 b) +{ + a.x += b.x; +} +inline __host__ __device__ void operator+=(float1 &a, const float b) +{ + a.x += b; +} +inline __host__ __device__ void operator/=(float1 &a, const float b) +{ + a.x /= b; +} + +inline __host__ __device__ float1 fminf(float1 a, float1 b) +{ + return make_float1(fminf(a.x,b.x)); +} +inline __host__ __device__ float1 fmaxf(float1 a, float1 b) +{ + return make_float1(fmaxf(a.x,b.x)); +} + +// cuda - glm compatibility + +inline __host__ __device__ float4 make_float4(const glm::vec3 a, const float b){ + return make_float4(a.x, a.y, a.z, b); +} +inline __host__ __device__ float4 make_float4(const glm::vec4 a){ + return make_float4(a.x, a.y, a.z, a.w); +} + +// vec4 + +inline __host__ __device__ void assign(float4 &a, const glm::vec4 b){ + a.x = b.x; + a.y = b.y; + a.z = b.z; + a.w = b.w; +} +inline __host__ __device__ void assign(glm::vec4 &a, const float4 b){ + a.x = b.x; + a.y = b.y; + a.z = b.z; + a.w = b.w; +} + +inline __host__ __device__ void assign(float4 &a, const glm::vec3 v, const float b){ + a.x = v.x; + a.y = v.y; + a.z = v.z; + a.w = b; +} +inline __host__ __device__ void assign(float2 &a, const float v, const float b){ + a.x = v; + a.y = b; +} + +inline __host__ __device__ float4 operator+(const float4 a, const glm::vec4 b) +{ + return make_float4(a.x + b.x, a.y + b.y, a.z + b.z, a.w + b.w); +} +inline __host__ __device__ glm::vec4 operator+(const glm::vec4 a, const float4 b) +{ + return glm::vec4(a.x + b.x, a.y + b.y, a.z + b.z, a.w + b.w); +} +inline __host__ __device__ void operator+=(float4 &a, const glm::vec4 b) +{ + a.x += b.x; + a.y += b.y; + a.z += b.z; + a.w += b.w; +} +inline __host__ __device__ void operator+=(glm::vec4 &a, const float4 b) +{ + a.x += b.x; + a.y += b.y; + a.z += b.z; + a.w += b.w; +} + +inline __host__ __device__ float4 operator-(const float4 a, const glm::vec4 b) +{ + return make_float4(a.x - b.x, a.y - b.y, a.z - b.z, a.w - b.w); +} +inline __host__ __device__ glm::vec4 operator-(const glm::vec4 a, const float4 b) +{ + return glm::vec4(a.x - b.x, a.y - b.y, a.z - b.z, a.w - b.w); +} +inline __host__ __device__ void operator-=(float4 &a, const glm::vec4 b) +{ + a.x -= b.x; + a.y -= b.y; + a.z -= b.z; + a.w -= b.w; +} +inline __host__ __device__ void operator-=(glm::vec4 &a, const float4 b) +{ + a.x -= b.x; + a.y -= b.y; + a.z -= b.z; + a.w -= b.w; +} + +inline __host__ __device__ float4 operator*(const float4 a, const glm::vec4 b) +{ + return make_float4(a.x * b.x, a.y * b.y, a.z * b.z, a.w * b.w); +} +inline __host__ __device__ glm::vec4 operator*(const glm::vec4 a, const float4 b) +{ + return glm::vec4(a.x * b.x, a.y * b.y, a.z * b.z, a.w * b.w); +} +inline __host__ __device__ void operator*=(float4 &a, const glm::vec4 b) +{ + a.x *= b.x; + a.y *= b.y; + a.z *= b.z; + a.w *= b.w; +} +inline __host__ __device__ void operator*=(glm::vec4 &a, const float4 b) +{ + a.x *= b.x; + a.y *= b.y; + a.z *= b.z; + a.w *= b.w; +} + +inline __host__ __device__ float4 operator/(const float4 a, const glm::vec4 b) +{ + return make_float4(a.x / b.x, a.y / b.y, a.z / b.z, a.w / b.w); +} +inline __host__ __device__ glm::vec4 operator/(const glm::vec4 a, const float4 b) +{ + return glm::vec4(a.x / b.x, a.y / b.y, a.z / b.z, a.w / b.w); +} +inline __host__ __device__ void operator/=(float4 &a, const glm::vec4 b) +{ + a.x /= b.x; + a.y /= b.y; + a.z /= b.z; + a.w /= b.w; +} +inline __host__ __device__ void operator/=(glm::vec4 &a, const float4 b) +{ + a.x /= b.x; + a.y /= b.y; + a.z /= b.z; + a.w /= b.w; +} + +inline __host__ __device__ glm::vec3 operator<(const glm::vec3 v, const float b) +{ + return glm::vec3(v.x(const glm::vec3 v, const float b) +{ + return glm::vec3(v.x>b, v.y>b, v.z>b); +} +inline __host__ __device__ glm::ivec3 operator<(const glm::ivec3 v, const int32_t b) +{ + return glm::ivec3(v.x + __device__ __host__ inline T make_cudaFloat(const float a); + template<> + __device__ __host__ inline float1 make_cudaFloat(const float a){return make_float1(a);} + template<> + __device__ __host__ inline float2 make_cudaFloat(const float a){return make_float2(a);} + template<> + __device__ __host__ inline float3 make_cudaFloat(const float a){return make_float3(a);} + template<> + __device__ __host__ inline float4 make_cudaFloat(const float a){return make_float4(a);} + + template + __device__ __host__ inline V lerp(const V a, const V b, const T t){ + return a + t*(b-a); //(T(1) - t)*a + t*b; + } + __device__ inline float flerp(const float a, const float b, const float t){ + return __fmaf_rn(__fsub_rn(b,a),t,a); + } + + constexpr __host__ __device__ float sum(const float4 v){ + return v.x+v.y+v.z+v.w; + } + constexpr __host__ __device__ float sum(const float3 v){ + return v.x+v.y+v.z; + } + constexpr __host__ __device__ float sum(const float2 v){ + return v.x+v.y; + } + constexpr __host__ __device__ float sum(const float1 v){ + return v.x; + } + constexpr __host__ __device__ float sum(const glm::vec4 v){ + return v.x+v.y+v.z+v.w; + } + constexpr __host__ __device__ float sum(const glm::vec3 v){ + return v.x+v.y+v.z; + } + + constexpr __host__ __device__ float prod(const glm::vec3 v){ + return v.x*v.y*v.z; + } + constexpr __host__ __device__ float prod(const float3 v){ + return v.x*v.y*v.z; + } + constexpr __host__ __device__ float prod(const float2 v){ + return v.x*v.y; + } + constexpr __host__ __device__ int32_t prod(const glm::ivec3 v){ + return v.x*v.y*v.z; + } + constexpr __host__ __device__ uint32_t prod(const uint3 v){ + return v.x*v.y*v.z; + } + inline __host__ __device__ uint32_t prod(const dim3 v){ + return v.x*v.y*v.z; + } + + constexpr __host__ __device__ int32_t exp2(uint32_t e){ + return 1< + constexpr __host__ __device__ T positivemod(const T v, const M m){ + // T mod = v%m; + // T isNegative = v<0; + // return mod + m*isNegative; + return (v%m) + m*(v<0); + } +} + +#endif //VMATH_HELPER \ No newline at end of file diff --git a/phitest/render/data_structures.py b/phitest/render/data_structures.py new file mode 100644 index 0000000..5e48e44 --- /dev/null +++ b/phitest/render/data_structures.py @@ -0,0 +1,2668 @@ +import copy, os, numbers +import tensorflow as tf +import numpy as np +from lib.tf_ops import shape_list, spacial_shape_list, has_shape, has_rank, tf_tensor_stats, tf_norm2, tf_angle_between, tf_image_resize_mip, tf_split_to_size +from lib.util import load_numpy, is_None_or_type +from .renderer import Renderer +from .camera import Camera +from .transform import Transform, GridTransform +from .vector import GridShape, Vector3 +import logging #, warnings + +LOG = logging.getLogger("Structs") + + + +# --- DATA Structs --- + +def get_coord_field(shape, offset=[0,0,0], lod=0.0, concat=True): + ''' + shape: z,y,x + offset: x,y,z + returns: 1,z,y,x,c with c=x,z,y,lod + ''' + coord_z, coord_y, coord_x = tf.meshgrid(tf.range(shape[0], dtype=tf.float32), tf.range(shape[1], dtype=tf.float32), tf.range(shape[2], dtype=tf.float32), indexing='ij') #z,y,x + coord_data = [tf.reshape(coord_x + offset[0], [1]+shape+[1]), + tf.reshape(coord_y + offset[1], [1]+shape+[1]), + tf.reshape(coord_z + offset[2], [1]+shape+[1])] #3 x 1DHW1 + if lod is not None: + lod_data = tf.constant(lod, shape=[1]+shape+[1], dtype=tf.float32) #tf.ones([1]+shape+[1])*lod + coord_data.append(lod_data)#4 x 1DHW1 + if concat: + coord_data = tf.concat(coord_data, axis=-1) + + #coord_data = tf.meshgrid(tf.range(shape[2], dtype=tf.float32), tf.range(shape[1], dtype=tf.float32), tf.range(shape[0], dtype=tf.float32), indexing='ij') #x,y,z + #coord_data = tf.transpose(coord_data, (3,2,1,0)) #c,x,y,z -> z,y,x,c + #coord_data = tf.reshape(coord_data, [1]+shape+[3]) + tf.constant(offset, dtype=tf.float32) + #if lod is not None: + # coord_data = tf.concat([coord_data, lod_data], axis=-1) + + return coord_data + +class ResourceCacheTF: + def __init__(self, device, value=None): + self.__device = device + self.set_value(value) + def set_value(self, value): + if not (isinstance(value, (tf.Tensor, np.ndarray)) or value is None): + raise TypeError("") + if value is None: + self.__value = value + else: + with tf.device(self.__device): + self.__value = tf.identity(value) + def get_value(self): + return self.__value + @property + def value(self): + return self.get_value() + @value.setter + def value(self, value): + self.set_value(value) + def clear(self): + self.set_value(None) + @property + def has_value(self): + return self.__value is not None + +class ResourceCacheDictTF: + def __init__(self, device): + self.__device = device + self.__values = {} + def set_value(self, key, value): + if not (isinstance(value, (tf.Tensor, np.ndarray)) or value is None): + raise TypeError("Value must be ndarray, Tensor or None. is %s"%(type(value).__name__,)) + if value is None: + self.__values[key] = value + else: + with tf.device(self.__device): + self.__values[key] = tf.identity(value) + def __contains__(self, key): + return self.has_value(key) + def __getitem__(self, key): + return self.get_value(key) + def __setitem__(self, key, value): + return self.set_value(key, value) + def __delitem__(self, key): + self.remove_value(key) + def has_value(self, key): + return key in self.__values + def __check_key(self, key): + if not self.has_value(key): + raise KeyError("Resource Cache does not contain '{}'".format(keys)) + def keys(self): + return self.__values.keys() + def get_value(self, key): + self.__check_key(key) + return self.__values[key] + def get_values(self): + return copy.copy(self.__values) + def items(self): + return self.__values.items() + def remove_value(self, key): + self.__check_key(key) + del self.__values[key] + def clear(self): + self.__values = {} + def __len__(self): + return len(self.__values) + def is_empty(self): + return len(self)==0 + +class ImageSet: + def __init__(self, base_images, shape=None, device=None, var_name="image_set", trainable=True, resize_method="LINEAR"): + assert isinstance(trainable, bool) + assert isinstance(var_name, str) + self.device = device + self.var_name = var_name + assert resize_method in ["LINEAR", "NEAREST"] + self.resize_method = resize_method + + assert shape is None or has_shape(shape, [2]) + assert isinstance(base_images, (tf.Tensor, tf.Variable, np.ndarray)), "ImageSet: images must be tf.Tensor or np.ndarray." + if isinstance(base_images, (tf.Tensor, tf.Variable)): + if not tf.reduce_all(tf.is_finite(base_images)).numpy(): + LOG.warning("Base images of '%s' are not finite.", var_name) + elif isinstance(base_images, np.ndarray): + if not np.all(np.isfinite(base_images)): + LOG.warning("Base images of '%s' are not finite.", var_name) + + image_shape = shape_list(base_images) + image_rank = len(image_shape) + if image_rank==4: + base_images = GridShape.from_tensor(base_images).normalize_tensor_shape(base_images) + elif not image_rank==5: raise ValueError("ImageSet: images must be rank 5, NVHWC.") + with tf.device(self.device): + self._base_images = tf.identity(base_images) + self.resize(self.base_shape.yx.value if shape is None else shape) + + self._MS_images = {} + + def _create_size(self, shape): + # resize images to requested shape, using base_images as basis for interpolation/filtering + if not (has_shape(shape, [2])): + raise ValueError("ImageSet.resize(): image shape must be (height, width), is %s with shape %s"%(shape,shape_list(shape))) + if np.all(self.base_shape.yx.value==shape): + images = self.base_images + else: + base_shape = self.base_shape + images = tf.reshape(self.base_images, (base_shape.n*base_shape.z, base_shape.y, base_shape.x, base_shape.c)) + if self.resize_method=="LINEAR": + images = tf_image_resize_mip(images, shape, mip_bias=0.5, method=tf.image.ResizeMethod.BILINEAR) + elif self.resize_method=="NEAREST": + images = tf_image_resize_mip(images, shape, mip_bias=float("-inf"), method=tf.image.ResizeMethod.NEAREST_NEIGHBOR) + images = tf.reshape(images, (base_shape.n, base_shape.z, shape[0], shape[1], base_shape.c)) + + return images + + def resize(self, shape): + # resize images to requested shape, using base_images as basis for interpolation/filtering + images = self._create_size(shape) + with tf.device(self.device): + self._scaled_images = tf.identity(images) + + def create_MS_stack(self, scale_shapes): + # scale_shapes: dict {scale: [shape]] + self._MS_images = {} + for scale, shape in scale_shapes.items(): + images = self._create_size(shape) + with tf.device(self.device): + self._MS_images[scale] = tf.identity(images) + + @property + def images(self): + return self._scaled_images + + def get_images_of_views(self, view_indices=None): + if view_indices is None: + return self.images + else: + imgs = self.images + #LOG.info("selecting views %s from image set '%s' with shape %s", view_indices, self.var_name, shape_list(imgs)) + #return imgs[:,view_indices,...] + imgs = tf.unstack(imgs, axis=1) + return tf.stack([imgs[_] for _ in view_indices], axis=1) + + #def __array__(self, dtype=None): + # if dtype: + # return tf.cast(self.images, dtype) + # else: + # return self.images + + @property + def shape(self): + return GridShape.from_tensor(self._scaled_images) + @property + def base_images(self): + return self._base_images + @property + def base_shape(self): + return GridShape.from_tensor(self._base_images) + + def shape_MS(self, scale): + return GridShape.from_tensor(self._MS_images[scale]) + + def images_MS(self, scale): + return self._MS_images[scale] + + def get_images_of_views_MS(self, scale, view_indices=None): + if view_indices is None: + return self.images_MS(scale) + else: + imgs = self.images_MS(scale) + #LOG.info("selecting views %s from image set '%s' with shape %s", view_indices, self.var_name, shape_list(imgs)) + #return imgs[:,view_indices,...] + imgs = tf.unstack(imgs, axis=1) + return tf.stack([imgs[_] for _ in view_indices], axis=1) + + @property + def num_channels(self): + return self.base_shape.c + @property + def num_views(self): + return self.base_shape.z + @property + def batch_size(self): + return self.base_shape.n + + + def save(self, renderer, path, format="PNG", idx=0, name=None): + if name is None: + name = self.var_name + shape = self.shape + renderer.write_images_batch_views(self.base_images, '%s_b{batch:04d}_v{view:02d}_{idx:04d}'%name, base_path=path, frame_idx=idx, input_format="NVHWC", image_format=format) + + def save_scaled(self, renderer, path, format="PNG", idx=0, name=None): + if name is None: + name = self.var_name + shape = self.shape + renderer.write_images_batch_views(self.images, '%s_b{batch:04d}_v{view:02d}_{idx:04d}'%name, base_path=path, frame_idx=idx, input_format="NVHWC", image_format=format) + + def save_MS_stack(self, renderer, path, format="PNG", idx=0, name=None): + if name is None: + name = self.var_name + for scale, image in self._MS_images.items(): + renderer.write_images_batch_views(image, '%s_%02d_b{batch:04d}_v{view:02d}_{idx:04d}'%(name, scale), base_path=path, frame_idx=idx, input_format="NVHWC", image_format=format) + + +class ImageSetMS(ImageSet): + def __init__(self, base_images, device=None, var_name="image_set_MS", trainable=True, resize_method="LINEAR"): + assert isinstance(base_images, dict) + + self._scales_MS = sorted(base_images.keys()) + self._scale_shapes_MS = {} + self._base_images_MS = {} + for s in self._scales_MS: + scale_image = base_images[s] + assert isinstance(scale_image, (tf.Tensor, tf.Variable, np.ndarray)), "ImageSet: images must be tf.Tensor or np.ndarray." + if isinstance(scale_image, (tf.Tensor, tf.Variable)): + if not tf.reduce_all(tf.is_finite(scale_image)).numpy(): + LOG.warning("Base images [%d] of '%s' are not finite.", s, var_name) + elif isinstance(scale_image, np.ndarray): + if not np.all(np.isfinite(scale_image)): + LOG.warning("Base images [%d] of '%s' are not finite.", s, var_name) + + scale_shape = shape_list(scale_image) + scale_rank = len(scale_shape) + if scale_rank==4: + scale_image = GridShape.from_tensor(scale_image).normalize_tensor_shape(scale_image) + elif not scale_rank==5: raise ValueError("ImageSet: images [%d] must be rank 4 or 5, NVHWC."%(s)) + + with tf.device(device): + self._base_images_MS[s] = tf.identity(scale_image) + self._scale_shapes_MS[s] = GridShape.from_tensor(scale_image) + + if s is not self._scales_MS[0]: + assert self._scale_shapes_MS[s].n == self._scale_shapes_MS[0].n, "Batch size mismatch" + assert self._scale_shapes_MS[s].z == self._scale_shapes_MS[0].z, "View size mismatch" + assert self._scale_shapes_MS[s].c == self._scale_shapes_MS[0].c, "Channel size mismatch" + + super().__init__(self._base_images_MS[self._scales_MS[-1]], shape=None, device=device, var_name=var_name, trainable=trainable, resize_method=resize_method) + + def has_base_MS_scale(self, scale): + return scale in self._scales_MS + def _check_base_MS_scale(self, scale): + if not self.has_base_MS_scale(scale): raise KeyError("Scale {} not in ImageSetMS '{}', available scales: {}".format(scale, self.var_name, self._scales_MS)) + + def base_shape_MS(self, scale): + self._check_base_MS_scale(scale) + return self._scale_shapes_MS[scale] + def base_images_MS(self, scale): + self._check_base_MS_scale(scale) + return self._base_images_MS[scale] + def get_base_images_of_views_MS(self, scale, view_indices=None): + self._check_base_MS_scale(scale) + if view_indices is None: + return self.base_images_MS(scale) + else: + imgs = self.base_images_MS(scale) + imgs = tf.unstack(imgs, axis=1) + return tf.stack([imgs[_] for _ in view_indices], axis=1) + def save_base_MS_stack(self, renderer, path, format="PNG", idx=0, name=None): + if name is None: + name = self.var_name + for scale, image in self._base_images_MS.items(): + renderer.write_images_batch_views(image, '%s_MS%02d_b{batch:04d}_v{view:02d}_{idx:04d}'%(name, scale), base_path=path, frame_idx=idx, input_format="NVHWC", image_format=format) + +class Zeroset: + def __init__(self, initial_value, shape=None, as_var=True, outer_bounds="OPEN", device=None, var_name="zeroset", trainable=True): + self.outer_bounds = outer_bounds + self.is_var = as_var + self._device = device + self._name = var_name + self._is_trainable = trainable + + with tf.device(self._device): + if shape is not None: + assert isinstance(shape, GridShape) + initial_value = tf.constant(initial_value, shape=shape.value, dtype=tf.float32) + if as_var: + self._levelset = tf.Variable(initial_value=initial_value, name=var_name, trainable=trainable) + else: + self._levelset = tf.identity(initial_value) + + @property + def grid_shape(self): + return GridShape.from_tensor(self._levelset) + + def _hull_staggered_lerp_weight(self, a, b): + a_leq = tf.less_equal(a,0) + return tf.where( tf.logical_xor(a_leq, tf.less_equal(b,0)), #sign change along iterpolation + tf.abs( tf.divide( tf.minimum(a,b), tf.subtract(a,b) ) ), + tf.cast(a_leq, dtype=a.dtype) + ) + + def _hull_simple_staggered_component(self, axis): + assert axis in [1,2,3,-2,-3,-4] + axis = axis%5 + pad = [(0,0),(0,0),(0,0),(0,0),(0,0)] + pad[axis]=(1,1) + shape = self.grid_shape.value + shape[axis] -= 1 + offset = np.zeros((5,), dtype=np.int32) + cells_prev = tf.slice(self._levelset, offset, shape) #self._levelset[:,:,:,:-1,:] + offset[axis] += 1 + cells_next = tf.slice(self._levelset, offset, shape) #self._levelset[:,:,:, 1:,:] + hull = self._hull_staggered_lerp_weight(cells_prev,cells_next) + hull = tf.pad(hull, pad, constant_values=1 if self.outer_bounds=="OPEN" else 0) + return hull + + def to_hull_simple_staggered(self): + return self._hull_simple_staggered_component(-2), self._hull_simple_staggered_component(-3), self._hull_simple_staggered_component(-4) + + def to_hull_simple_centered(self): + raise NotImplementedError() + + def to_denstiy_simple_centered(self): + return tf.where(tf.greater(self._levelset, 0), 250, 0) + + def resize(self, shape): + assert shape_list(shape)==[3] + new_shape = GridShape(shape) + if new_shape==self.grid_shape: + return + raise NotImplementedError("Zeroset.resize() not implemented.") + + def assign(levelset): + raise NotImplementedError() + + + + +class DensityGrid(): + def __init__(self, shape, constant=0.1, as_var=True, d=None, scale_renderer=None, hull=None, inflow=None, inflow_offset=None, inflow_mask=None, device=None, var_name="denstiy", trainable=True, restrict_to_hull=True, is_SDF=False): + + #super().__init__(inputs=[], models=[], device=device) + + assert isinstance(as_var, bool), "DensityGrid: as_var must be bool, is: %s"%(type(as_var).__name__,) + assert isinstance(trainable, bool), "DensityGrid: trainable must be bool, is: %s"%(type(trainable).__name__,) + #if restrict_to_hull is None: restrict_to_hull=False + assert isinstance(restrict_to_hull, bool), "DensityGrid: restrict_to_hull must be bool, is: %s"%(type(restrict_to_hull).__name__,) + assert isinstance(var_name, str), "DensityGrid: var_name must be str, is: %s"%(type(var_name).__name__,) + assert d is None or isinstance(d, (tf.Tensor, tf.Variable, np.ndarray)) + assert scale_renderer is None or isinstance(scale_renderer, Renderer) + + self.shape = list(shape) + if d is not None: + d_shape = shape_list(d) + if not len(d_shape)==5 or not d_shape[-1]==1 or not self.shape==spacial_shape_list(d): + raise ValueError("Invalid shape of density on assignment: %s"%d_shape) + self.is_var = as_var + self._device = device + self._name = var_name + self._is_trainable = trainable + self._is_SDF = False #is_SDF + if as_var: + rand_init = tf.constant_initializer(constant) + with tf.device(self._device): + self._d = tf.Variable(initial_value=d if d is not None else rand_init(shape=[1]+self.shape+[1], dtype=tf.float32), name=var_name+'_dens', trainable=True) + + else: + with tf.device(self._device): + if d is not None: + self._d = tf.identity(d) + else: + self._d = tf.constant(constant, shape=[1]+self.shape+[1], dtype=tf.float32) + + self.scale_renderer = scale_renderer + with tf.device(self._device): + self.hull = tf.constant(hull, dtype=tf.float32) if hull is not None else None + self.restrict_to_hull = restrict_to_hull + + if inflow is not None: + with tf.device(self._device): + if isinstance(inflow, str) and inflow=='CONST': + assert isinstance(inflow_mask, (tf.Tensor, tf.Variable, np.ndarray)) + inflow = rand_init(shape=shape_list(inflow_mask), dtype=tf.float32) + if as_var: + self._inflow = tf.Variable(initial_value=inflow, name=var_name+'_inflow', trainable=True) + else: + self._inflow = tf.constant(inflow, dtype=tf.float32) + self.inflow_mask = tf.constant(inflow_mask, dtype=tf.float32) if inflow_mask is not None else None + inflow_shape = spacial_shape_list(self._inflow) #.get_shape().as_list()[-4:-1] + self._inflow_padding = [[0,0]]+[[inflow_offset[_],self.shape[_]-inflow_offset[_]-inflow_shape[_]] for _ in range(3)]+[[0,0]] + self.inflow_offset = inflow_offset + else: + self._inflow = None + + + @property + def trainable(self): + return self._is_trainable and self.is_var + @property + def is_SDF(self): + return False #self._is_SDF + + @property + def d(self): + if self.restrict_to_hull: + return self.with_hull() + else: + return tf.identity(self._d) + + def with_hull(self): + if self.hull is not None: + return self._d * self.hull # hull is a (smooth) binary mask + else: + return tf.identity(self._d) + + @property + def inflow(self): + if self._inflow is None: + return tf.zeros_like(self._d, dtype=tf.float32) + elif self.inflow_mask is not None: #hasattr(self, 'inflow_mask') and + return tf.pad(self._inflow*self.inflow_mask, self._inflow_padding) + else: + return tf.pad(self._inflow, self._inflow_padding) + + def with_inflow(self): + density = self.d + if self._inflow is not None: + density = tf.maximum(density+self.inflow, 0) + return density + + @classmethod + def from_file(cls, path, as_var=True, scale_renderer=None, hull=None, inflow=None, inflow_offset=None, inflow_mask=None, device=None, var_name="denstiy", trainable=True, restrict_to_hull=True, is_SDF=False): + try: + with np.load(path) as np_data: + d = np_data['arr_0'] + shape =spacial_shape_list(d) + if 'hull' in np_data and hull is None: + hull = np_data['hull'] + if 'inflow' in np_data and inflow is None: + inflow=np_data['inflow'] + if 'inflow_mask' in np_data and inflow_mask is None: + inflow_mask=np_data['inflow_mask'] + if 'inflow_offset' in np_data and inflow_offset is None: + inflow_offset=np_data['inflow_offset'].tolist() + #grid = cls(shape, d=d, as_var=as_var, scale_renderer=scale_renderer, hull=hull, inflow=np_data['inflow'], inflow_offset=np_data['inflow_offset'].tolist(), device=device) + grid = cls(shape, d=d, as_var=as_var, scale_renderer=scale_renderer, hull=hull, inflow=inflow, inflow_offset=inflow_offset, inflow_mask=inflow_mask, \ + device=device, var_name=var_name, trainable=trainable, restrict_to_hull=restrict_to_hull, is_SDF=is_SDF) + except: + LOG.warning("Failed to load density from '%s':", path, exc_info=True) + return None + else: + return grid + + @classmethod + def from_scalarFlow_file(cls, path, as_var=True, shape=None, scale_renderer=None, hull=None, inflow=None, inflow_offset=None, inflow_mask=None, device=None, var_name="sF_denstiy", trainable=True, restrict_to_hull=True, is_SDF=is_SDF): + # if shape is set the loaded grid will be reshaped if necessary + # with np.load(path) as np_data: + # density = np_data['data'].astype(np.float32)[::-1] # DHWC with C=1 and D/z reversed + density = load_numpy(path).astype(np.float32)[::-1] + density = density.reshape([1] + list(density.shape)) # + density = tf.constant(density, dtype=tf.float32) + d_shape = spacial_shape_list(density) + if shape is not None and shape!=d_shape: + if scale_renderer is None: + raise ValueError("No renderer provided to scale density.") + LOG.debug("scaling scalarFlow density from %s to %s", d_shape, shape) + density = scale_renderer.resample_grid3D_aligned(density, shape) + d_shape = shape + else: + # cut of SF inflow region and set as inflow. or is it already cut off in SF dataset? it is, but not in the synth dataset or my own sF runs. + # lower 15 cells... + inflow, density= tf.split(density, [15, d_shape[1]-15], axis=-3) + inflow_mask = tf.ones_like(inflow, dtype=tf.float32) + inflow_offset = [0,0,0] + density = tf.concat([tf.zeros_like(inflow, dtype=tf.float32), density], axis=-3) + return cls(d_shape, d=density, as_var=as_var, scale_renderer=scale_renderer, hull=hull, inflow=inflow, inflow_offset=inflow_offset, inflow_mask=inflow_mask, \ + device=device, var_name=var_name, trainable=trainable, restrict_to_hull=restrict_to_hull) + + def copy(self, as_var=None, device=None, var_name=None, trainable=None, restrict_to_hull=None): + if as_var is None: + as_var = self.is_var + if var_name is None: + var_name = self._name + '_cpy' + if trainable is None: + trainable = self._is_trainable + if restrict_to_hull is None: + restrict_to_hull = self.restrict_to_hull + if self._inflow is not None: + grid = DensityGrid(self.shape, d=tf.identity(self._d), as_var=as_var, scale_renderer=self.scale_renderer, hull=self.hull, \ + inflow=tf.identity(self._inflow), inflow_offset=self.inflow_offset, inflow_mask=self.inflow_mask, \ + device=device, var_name=var_name, trainable=trainable, restrict_to_hull=restrict_to_hull, is_SDF=self._is_SDF) + else: + grid = DensityGrid(self.shape, d=tf.identity(self._d), as_var=as_var, scale_renderer=self.scale_renderer, hull=self.hull, \ + device=device, var_name=var_name, trainable=trainable, restrict_to_hull=restrict_to_hull, is_SDF=self._is_SDF) + return grid + + def copy_empty(self, as_var=None, device=None, var_name=None, trainable=None, restrict_to_hull=None): + if as_var is None: + as_var = self.is_var + if var_name is None: + var_name = self._name + '_cpy' + if trainable is None: + trainable = self._is_trainable + if restrict_to_hull is None: + restrict_to_hull = self.restrict_to_hull + if self._inflow is not None: + grid = DensityGrid(self.shape, constant=0.0, as_var=as_var, scale_renderer=self.scale_renderer, hull=self.hull, \ + inflow=tf.identity(self._inflow), inflow_offset=self.inflow_offset, inflow_mask=self.inflow_mask, \ + device=device, var_name=var_name, trainable=trainable, restrict_to_hull=restrict_to_hull, is_SDF=self._is_SDF) + else: + grid = DensityGrid(self.shape, constant=0.0, as_var=as_var, scale_renderer=self.scale_renderer, hull=self.hull, \ + device=device, var_name=var_name, trainable=trainable, restrict_to_hull=restrict_to_hull, is_SDF=self._is_SDF) + return grid + + def scaled(self, new_shape, with_inflow=False): + with self.scale_renderer.profiler.sample("get density scaled"): + if not (isinstance(new_shape, list) and len(new_shape)==3): + raise ValueError("Invalid shape") + density = self.d if not with_inflow else self.with_inflow() + if new_shape!=self.shape: + LOG.debug("Scaling density from %s to %s", self.shape, new_shape) + with self.scale_renderer.profiler.sample("scale density"): + d_scaled = self.scale_renderer.resample_grid3D_aligned(density, new_shape) + if self._is_SDF: + with self.scale_renderer.profiler.sample("scale SDF"): + # scale values to be valid OS distances + scale_factor = np.mean([o/i for o,i in zip(new_shape, self.shape)]) + d_scaled = d_scaled * scale_factor + else: + LOG.debug("No need to scale density to same shape %s", self.shape) + d_scaled = density #tf.identity(density) # self.d already copies + return d_scaled + + def rescale(self, new_shape, base_scale_fn): + '''re-scale/re-shape in place''' + if not isinstance(new_shape, (list,tuple)) or not len(new_shape)==3: + raise ValueError("Invalid shape for density re-scaling: %s"%new_shape) + d = self.scaled(new_shape) + hull = self.scale_renderer.resample_grid3D_aligned(self.base_hull, new_shape) if self.hull is not None else None + if self._inflow is not None: + if_off = base_scale_fn(self.base_inflow_offset) + if_shape = base_scale_fn(self.base_inflow_shape) + if_scaled = self.scale_renderer.resample_grid3D_aligned(self._inflow, if_shape) + if_mask = None if self.inflow_mask is None else self.scale_renderer.resample_grid3D_aligned(self.base_inflow_mask, if_shape) + LOG.info("Frame %04d: inflow to %s, offset to %s", state.frame, if_shape, if_off) + + self.shape = list(new_shape) + with tf.device(self._device): + if self.is_var: + self._d = tf.Variable(initial_value=d, name=var_name+'_rs', trainable=True) + else: + self._d = tf.identity(d) + + if hull is not None: + self.hull = tf.identity(hull) + + if self._inflow is not None: + if as_var: + self._inflow = tf.Variable(initial_value=if_scaled, name=var_name+'_inflow', trainable=True) + else: + self._inflow = tf.identity(if_scaled) + self.inflow_mask = tf.identity(if_mask) if if_mask is not None else None + inflow_shape = spacial_shape_list(self._inflow) #.get_shape().as_list()[-4:-1] + self._inflow_padding = [[0,0]]+[[if_off[_],self.shape[_]-if_off[_]-inflow_shape[_]] for _ in range(3)]+[[0,0]] + self.inflow_offset = if_off + + def copy_scaled(self, new_shape, as_var=None, device=None, var_name=None, trainable=None, restrict_to_hull=None): + '''Does not copy inflow and hull, TODO''' + if as_var is None: + as_var = self.is_var + if as_var and var_name is None: + var_name = self._name + '_scaled' + if trainable is None: + trainable = self._is_trainable + if restrict_to_hull is None: + restrict_to_hull = self.restrict_to_hull + d_scaled = self.scaled(new_shape) + grid = DensityGrid(new_shape, d=d_scaled, as_var=as_var, scale_renderer=self.scale_renderer, device=device, var_name=var_name, trainable=trainable, restrict_to_hull=restrict_to_hull, is_SDF=self._is_SDF) + return grid + + def warped(self, vel_grid, order=1, dt=1.0, clamp="NONE"): + if not (isinstance(vel_grid, VelocityGrid)): + raise ValueError("Invalid velocity grid") + return vel_grid.warp(self.with_inflow(), order=order, dt=dt, clamp=clamp) + + def copy_warped(self, vel_grid, as_var=None, order=1, dt=1.0, device=None, var_name=None, clamp="NONE", trainable=None, restrict_to_hull=None): + '''Does not copy inflow and hull, TODO''' + if as_var is None: + as_var = self.is_var + if var_name is None: + var_name = self._name + '_warped' + if trainable is None: + trainable = self._is_trainable + if restrict_to_hull is None: + restrict_to_hull = self.restrict_to_hull + d_warped = self.warped(vel_grid, order=order, dt=dt, clamp=clamp) + grid = DensityGrid(self.shape, d=d_warped, as_var=as_var, scale_renderer=self.scale_renderer, device=device, var_name=var_name, trainable=trainable, restrict_to_hull=restrict_to_hull, is_SDF=self._is_SDF) + return grid + + def scale(self, scale): + self.assign(self._d*scale) + + def apply_clamp(self, vmin, vmax): + if not self._is_SDF: + vmin = tf.maximum(vmin, 0) + elif vmin >= 0.0: + LOG.warning("apply_clamp called on SDF DensityGrid with min >= 0.") + d = tf.clip_by_value(self._d, vmin, vmax) + inflow = None + if self._inflow is not None: + # inflow_shape = self._inflow.get_shape().as_list() + # density = self._d[:,self.inflow_offset[0]:self.inflow_offset[0]+inflow_shape[-4], \ + # self.inflow_offset[1]:self.inflow_offset[1]+inflow_shape[-3], \ + # self.inflow_offset[2]:self.inflow_offset[2]+inflow_shape[-2],:] + # use already clamped density for consistency + denstiy_shape = shape_list(d) + density_cropped = d[self._inflow_padding[0][0] : denstiy_shape[0]-self._inflow_padding[0][1], + self._inflow_padding[1][0] : denstiy_shape[1]-self._inflow_padding[1][1], + self._inflow_padding[2][0] : denstiy_shape[2]-self._inflow_padding[2][1], + self._inflow_padding[3][0] : denstiy_shape[3]-self._inflow_padding[3][1], + self._inflow_padding[4][0] : denstiy_shape[4]-self._inflow_padding[4][1]] + inflow = tf.clip_by_value(self._inflow, vmin - density_cropped, vmax - density_cropped) + self.assign(d, inflow) + + def assign(self, d, inflow=None): + shape = shape_list(d) + if not len(shape)==5 or not shape[-1]==1 or not shape[-4:-1]==self.shape: + raise ValueError("Invalid or incompatible shape of density on assignment: is {}, required: NDHW1 with DHW={}".format(shape, self.shape)) + if self.is_var: + self._d.assign(d) + if self._inflow is not None and inflow is not None: + self._inflow.assign(inflow) + else: + with tf.device(self._device): + self._d = tf.identity(d) + if self._inflow is not None and inflow is not None: + self._inflow = tf.identity(inflow) + + def assign_scaled(self, d): + shape = shape_list(d) + if not len(shape)==5 or not shape[-1]==1: + raise ValueError("Invalid shape of density on assignment: is {}, required: NDHW1".format(shape)) + d = self.scale_renderer.resample_grid3D_aligned(d, self.shape) + self.assign(d) + + def var_list(self): + if self.is_var: + if self._inflow is not None: + return [self._d, self._inflow] + return [self._d] + else: + raise TypeError("This DensityGrid is not a variable.") + + def get_variables(self): + if self.is_var: + var_dict = {'density': self._d} + if self._inflow is not None: + var_dict['inflow'] = self._inflow + return var_dict + else: + #raise TypeError("This DensityGrid is not a variable.") + return dict() + + def get_output_variables(self, include_MS=False, include_residual=False, only_trainable=False): + var_dict = {'density': self._d} + if self._inflow is not None: + var_dict['inflow'] = self._inflow + return var_dict + + + def save(self, path): + density = self._d + if isinstance(density, (tf.Tensor, tf.Variable)): + density = density.numpy() + save = {} + if self.hull is not None: + hull = self.hull + if isinstance(hull, (tf.Tensor, tf.Variable)): + hull = hull.numpy() + save['hull']=hull + if self._inflow is not None: + inflow = self._inflow + if isinstance(inflow, (tf.Tensor, tf.Variable)): + inflow = inflow.numpy() + save['inflow']=inflow + if self.inflow_mask is not None: + inflow_mask = self.inflow_mask + if isinstance(inflow_mask, (tf.Tensor, tf.Variable)): + inflow_mask = inflow_mask.numpy() + save['inflow_mask']=inflow_mask + save['inflow_offset']=np.asarray(self.inflow_offset) + #np.savez_compressed(path, density, inflow=inflow, inflow_offset=np.asarray(self.inflow_offset)) + np.savez_compressed(path, density, **save) + + def mean(self): + return tf.reduce_mean(self.d) + + def stats(self, mask=None, state=None, **warp_kwargs): + ''' + mask: optional binary float mask, stats only consider cells>0.5 + ''' + d = self.d + if mask is not None: + mask = mask if mask.dtype==tf.bool else tf.greater(mask, 0.5) + d = tf.boolean_mask(d, mask) + + stats = { + #'dMean':tf.reduce_mean(d), 'dMax':tf.reduce_max(d), 'dMin':tf.reduce_min(d),'dAbsMean':tf.reduce_mean(tf.abs(d)), + 'density': tf_tensor_stats(d, as_dict=True), + 'shape':self.shape, + } + if state is not None and state.prev is not None and state.prev.density is not None and state.prev.velocity is not None: + warp_SE = tf.squared_difference(state.prev.density_advected(**warp_kwargs), self.d) + if mask is not None: + warp_SE = tf.boolean_mask(warp_SE, mask) + stats["warp_SE"] = tf_tensor_stats(warp_SE, as_dict=True) + else: + stats["warp_SE"] = tf_tensor_stats(tf.zeros([1,1,1,1,1], dtype=tf.float32), as_dict=True) + return stats + + def clear_cache(self): + pass #super().clear_cache() + @property + def is_MS(self): + return False + @property + def has_MS_output(self): + return self.is_MS and False + +class VelocityGrid: + @staticmethod + def component_shapes(centered_shape): + assert len(centered_shape)==3 + x_shape = copy.copy(centered_shape) + x_shape[2] +=1 + y_shape = copy.copy(centered_shape) + y_shape[1] +=1 + z_shape = copy.copy(centered_shape) + z_shape[0] +=1 + return x_shape, y_shape, z_shape + @staticmethod + def component_potential_shapes(centered_shape): + assert len(centered_shape)==3 + x_shape = copy.copy(centered_shape) + x_shape[0] +=1 + x_shape[1] +=1 + y_shape = copy.copy(centered_shape) + y_shape[0] +=1 + y_shape[2] +=1 + z_shape = copy.copy(centered_shape) + z_shape[1] +=1 + z_shape[2] +=1 + return x_shape, y_shape, z_shape + + def __init__(self, centered_shape, std=0.1, as_var=True, x=None, y=None, z=None, boundary=None, scale_renderer=None, warp_renderer=None, *, coords=None, lod=None, device=None, var_name="velocity", trainable=True): + self.centered_shape = centered_shape.tolist() if isinstance(centered_shape, np.ndarray) else centered_shape + self.x_shape, self.y_shape, self.z_shape = VelocityGrid.component_shapes(self.centered_shape) + self.set_boundary(boundary) + self.is_var = as_var + self._device = device + self._name = var_name + self._is_trainable = trainable + if as_var: + if x is not None: + x_shape = shape_list(x) + if not len(x_shape)==5 or not x_shape[-1]==1 or not x_shape[-4:-1]==self.x_shape: + raise ValueError("Invalid shape of velocity x component on assignment") + if y is not None: + y_shape = shape_list(y) + if not len(y_shape)==5 or not y_shape[-1]==1 or not y_shape[-4:-1]==self.y_shape: + raise ValueError("Invalid shape of velocity y component on assignment") + if z is not None: + z_shape = shape_list(z) + if not len(z_shape)==5 or not z_shape[-1]==1 or not z_shape[-4:-1]==self.z_shape: + raise ValueError("Invalid shape of velocity z component on assignment") + # in a box + #rand_init = tf.random_normal_initializer(0.0, std) + std = tf.abs(std) + rand_init = tf.random_uniform_initializer(-std, std) + # maybe even uniformly in space and in a sphere?: http://6degreesoffreedom.co/circle-random-sampling/ + with tf.device(self._device): + self._x = tf.Variable(initial_value=x if x is not None else rand_init(shape=[1]+self.x_shape+[1], dtype=tf.float32), name=var_name + '_x', trainable=True) + self._y = tf.Variable(initial_value=y if y is not None else rand_init(shape=[1]+self.y_shape+[1], dtype=tf.float32), name=var_name + '_y', trainable=True) + self._z = tf.Variable(initial_value=z if z is not None else rand_init(shape=[1]+self.z_shape+[1], dtype=tf.float32), name=var_name + '_z', trainable=True) + else: + if x is None: + x = tf.constant(tf.random.uniform([1]+self.x_shape+[1], -std, std, dtype=tf.float32)) + if y is None: + y = tf.constant(tf.random.uniform([1]+self.y_shape+[1], -std, std, dtype=tf.float32)) + if z is None: + z = tf.constant(tf.random.uniform([1]+self.z_shape+[1], -std, std, dtype=tf.float32)) + self.assign(x,y,z) + + # if coords is None: + # self.coords = get_coord_field(self.centered_shape, lod=None) + # else: + # self.coords = coords + if lod is None: + lod = tf.zeros([1]+self.centered_shape+[1]) + with tf.device(self._device): + self.lod_pad = tf.identity(lod) + + self.scale_renderer = scale_renderer#Renderer(profiler, filter_mode='LINEAR', mipmapping='NONE', sample_gradients=False) + if self.scale_renderer is not None: + if (self.outer_bounds=='CLOSED' and self.scale_renderer.boundary_mode!='BORDER') \ + or (self.outer_bounds=='OPEN' and self.scale_renderer.boundary_mode!='CLAMP'): + LOG.warning("Velocity outer boundary %s does not match scale renderer boundary mode %s", self.outer_bounds, self.scale_renderer.boundary_mode) + self.warp_renderer = warp_renderer + if self.warp_renderer is not None: + if (self.outer_bounds=='CLOSED' and self.warp_renderer.boundary_mode!='BORDER') \ + or (self.outer_bounds=='OPEN' and self.warp_renderer.boundary_mode!='CLAMP'): + LOG.warning("Velocity outer boundary %s does not match scale renderer boundary mode %s", self.outer_bounds, self.warp_renderer.boundary_mode) + + def set_boundary(self, boundary): + assert (boundary is None) or isinstance(boundary, Zeroset) + self.boundary = boundary + self.outer_bounds = self.boundary.outer_bounds if self.boundary is not None else "OPEN" + + @staticmethod + def shape_centered_to_staggered(shape): + return [_+1 for _ in shape] + @property + def staggered_shape(self): + return self.shape_centered_to_staggered(self.centered_shape) + + @property + def trainable(self): + return self._is_trainable and self.is_var + + + def _staggered(self): + return (self._x, self._y, self._z) + + @property + def x(self): + v = self._x + if self.boundary is not None: + v*= self.boundary._hull_simple_staggered_component(-2) + return v + @property + def y(self): + v = self._y + if self.boundary is not None: + v*= self.boundary._hull_simple_staggered_component(-3) + return v + @property + def z(self): + v = self._z + if self.boundary is not None: + v*= self.boundary._hull_simple_staggered_component(-4) + return v + + @classmethod + def from_centered(cls, centered_grid, as_var=True, boundary=None, scale_renderer=None, warp_renderer=None, device=None, var_name="velocity", trainable=True): + centered_shape = shape_list(centered_grid) + assert len(centered_shape)==5, "input grid must be NDWHC, is %s"%(centered_shape,) + assert centered_shape[-1]==3 + #assert centered_shape[0]==1 + centered_shape = centered_shape[-4:-1] + vel_grid = cls(centered_shape, as_var=as_var, boundary=boundary, scale_renderer=scale_renderer, warp_renderer=warp_renderer, device=device, var_name=var_name, trainable=trainable) + x,y,z = vel_grid._centered_to_staggered(centered_grid) + vel_grid.assign(x,y,z) + return vel_grid + @classmethod + def from_staggered_combined(cls, staggered_grid, as_var=True, boundary=None, scale_renderer=None, warp_renderer=None, device=None, var_name="velocity", trainable=True): + centered_shape = shape_list(staggered_grid) + assert len(centered_shape)==5, "input grid must be NDWHC, is %s"%(centered_shape,) + assert centered_shape[-1]==3 + #assert centered_shape[0]==1 + centered_shape = [_-1 for _ in centered_shape[-4:-1]] + vel_grid = cls(centered_shape, as_var=as_var, boundary=boundary, scale_renderer=scale_renderer, warp_renderer=warp_renderer, device=device, var_name=var_name, trainable=trainable) + vel_grid.assign_staggered_combined(staggered_grid) + return vel_grid + + @classmethod + def from_file(cls, path, as_var=True, boundary=None, scale_renderer=None, warp_renderer=None, device=None, var_name="velocity", trainable=True): + try: + with np.load(path) as vel: + if 'centered_shape' not in vel:#legacy + shape = shape_list(vel["vel_x"]) + LOG.debug("%s", shape) + shape[-2] -=1 + shape = shape[1:-1] + else: + shape = vel['centered_shape'].tolist() + vel_grid = cls(shape, x=vel["vel_x"].astype(np.float32), y=vel["vel_y"].astype(np.float32), z=vel["vel_z"].astype(np.float32), \ + as_var=as_var, boundary=boundary, scale_renderer=scale_renderer, warp_renderer=warp_renderer, device=device, var_name=var_name, trainable=trainable) + except: + LOG.warning("Failed to load velocity from '%s':", path, exc_info=True) + return None + else: + return vel_grid + + @classmethod + def from_scalarFlow_file(cls, path, as_var=True, shape=None, boundary=None, scale_renderer=None, warp_renderer=None, device=None, var_name="sF_velocity", trainable=True): + # sF velocities are stored as combined staggered grid with upper cells missing, DHWC with C=3 + # with np.load(path) as np_data: + # velocity = np_data['data'].astype(np.float32)[::-1] # DHWC with C=3 and D/z reversed + velocity = load_numpy(path).astype(np.float32)[::-1] + v_shape = GridShape.from_tensor(velocity) + velocity = v_shape.normalize_tensor_shape(velocity) #.reshape([1] + list(velocity.shape)) # NDHWC + velocity = tf.constant(velocity, dtype=tf.float32) + v_shape = v_shape.zyx.value + v_x, v_y, v_z = tf.split(velocity, 3, axis=-1) + p0 = (0,0) + # extend missing upper cell + v_x = tf.pad(v_x, [p0,p0,p0,(0,1),p0], "SYMMETRIC") + v_y = tf.pad(v_y, [p0,p0,(0,1),p0,p0], "SYMMETRIC") + v_z = tf.pad(-v_z, [p0,(1,0),p0,p0,p0], "SYMMETRIC") #z value/direction reversed, pad lower value as axis is reversed (?) + #v_shape = spacial_shape_list(velocity) + if shape is not None and v_shape!=shape: + assert len(shape)==3 + if scale_renderer is None: + raise ValueError("No renderer provided to scale velocity.") + # shape = GridShape(shape).zyx + # vel_scale = shape/v_shape #[o/i for i,o in zip(v_shape, shape)] #z,y,x + LOG.debug("scaling scalarFlow velocity from %s to %s with magnitude scale %s", v_shape, shape) + v_tmp = cls(v_shape, x=v_x, y=v_y, z=v_z, as_var=False, boundary=boundary, scale_renderer=scale_renderer, warp_renderer=warp_renderer, device=device, var_name="sF_tmp", trainable=False) + v_x, v_y, v_z = v_tmp.scaled(shape, scale_magnitude=True) + # can only scale 1 and 4 channel grids + # v_x = scale_renderer.resample_grid3D_aligned(v_x, shape.value)*vel_scale.x#[2] + # v_y = scale_renderer.resample_grid3D_aligned(v_y, shape.value)*vel_scale.y#[1] + # v_z = scale_renderer.resample_grid3D_aligned(v_z, shape.value)*vel_scale.z#[0] + # velocity = tf.concat([v_x, v_y, v_z], axis=-1) + v_shape = shape + + #return cls.from_centered(velocity,as_var=as_var, boundary=boundary, scale_renderer=scale_renderer, warp_renderer=warp_renderer, device=device, var_name=var_name) + return cls(v_shape, x=v_x, y=v_y, z=v_z,as_var=as_var, boundary=boundary, scale_renderer=scale_renderer, warp_renderer=warp_renderer, device=device, var_name=var_name, trainable=trainable) + + def copy(self, as_var=None, device=None, var_name=None, trainable=None): + if as_var is None: + as_var = self.is_var + if as_var and var_name is None: + var_name = self._name + '_cpy' + if trainable is None: + trainable = self._is_trainable + grid = VelocityGrid(self.centered_shape, x=tf.identity(self._x), y=tf.identity(self._y), z=tf.identity(self._z), as_var=as_var, \ + boundary=self.boundary, scale_renderer=self.scale_renderer, warp_renderer=self.warp_renderer, device=device, var_name=var_name, trainable=trainable) + return grid + + + def scaled(self, centered_shape, scale_magnitude=True): + if not (isinstance(centered_shape, list) and len(centered_shape)==3): + raise ValueError("Invalid shape") + #resample velocity + if centered_shape!=self.centered_shape: + with self.scale_renderer.profiler.sample("scale velocity"): + x_shape, y_shape, z_shape = VelocityGrid.component_shapes(centered_shape) + LOG.debug("Scaling velocity from %s to %s", self.centered_shape, centered_shape) + x_scaled = self.scale_renderer.resample_grid3D_aligned(self.x, x_shape, align_x='center') + y_scaled = self.scale_renderer.resample_grid3D_aligned(self.y, y_shape, align_y='center') + z_scaled = self.scale_renderer.resample_grid3D_aligned(self.z, z_shape, align_z='center') + if scale_magnitude: + vel_scale = [o/i for i,o in zip(self.centered_shape, centered_shape)] #z,y,x + LOG.debug("Scaling velocity magnitude with %s", vel_scale) + x_scaled *= vel_scale[2] + y_scaled *= vel_scale[1] + z_scaled *= vel_scale[0] + else: + LOG.debug("No need to scale velocity to same shape %s", self.centered_shape) + x_scaled = tf.identity(self.x) + y_scaled = tf.identity(self.y) + z_scaled = tf.identity(self.z) + return x_scaled, y_scaled, z_scaled + + @staticmethod + def resample_velocity(scale_renderer, vel, scale=None, shape=None, is_staggered=True, scale_magnitude=True): + if scale is None and shape is None: raise ValueError("You must provide 1 of scale or shape.") + if scale is not None and shape is not None: raise ValueError("You must provide 1 of scale or shape.") + + #vel base shape + if isinstance(vel, tuple): + x_shape = shape_list(vel[0]) + assert len(x_shape)==5 + centered_shape = x_shape + centered_shape[-2] -= 1 + elif isinstance(vel, (np.ndarray, tf.Tensor)): + centered_shape = shape_list(vel) + assert len(centered_shape)==5 + else: + raise ValueError("Unknown velcity format '%s'"%(type(vel).__name__,)) + + if scale is not None: + assert isinstance(scale, numbers.Number), "invalid scaling factor" + shape = [int(round(_*scale)) for _ in centered_shape[1:-1]] + if shape is not None: + assert isinstance(shape, (list, tuple)) and len(shape)==3, "invalid shape" + scale = np.mean([float(t)/float(s) for s,t in zip(centered_shape[1:-1], shape)]) + + # our sampler can only handle 1,2 and 4 channels, not 3. + if is_staggered: + if isinstance(vel, tuple): + # separate staggered grids with different dimenstions + x_shape, y_shape, z_shape = VelocityGrid.component_shapes(shape) + x,y,z = vel + vel_components = [ + scale_renderer.resample_grid3D_aligned(x, x_shape, align_x="CENTER", align_y="BORDER", align_z="BORDER"), + scale_renderer.resample_grid3D_aligned(y, y_shape, align_x="BORDER", align_y="CENTER", align_z="BORDER"), + scale_renderer.resample_grid3D_aligned(z, z_shape, align_x="BORDER", align_y="BORDER", align_z="CENTER")] + + if scale_magnitude: + vel_components = [_*scale for _ in vel_components] + return tuple(vel_components) + + elif isinstance(vel, (np.ndarray, tf.Tensor)): + # combined staggered grid, like mantaflow + raise NotImplementedError("TODO: check correct shape") + x,y,z = tf.split(vel, 3, axis=-1) + vel_components = [ + scale_renderer.resample_grid3D_aligned(x, shape, align_x="CENTER", align_y="BORDER", align_z="BORDER"), + scale_renderer.resample_grid3D_aligned(y, shape, align_x="BORDER", align_y="CENTER", align_z="BORDER"), + scale_renderer.resample_grid3D_aligned(z, shape, align_x="BORDER", align_y="BORDER", align_z="CENTER")] + + vel = tf.concat(vel_components, axis=-1) + if scale_magnitude: + vel = vel*scale + return vel + else: + vel_components = tf.split(vel, [2,1], axis=-1) + vel_components = [scale_renderer.resample_grid3D_aligned(_, shape) for _ in vel_components] + + vel = tf.concat(vel_components, axis=-1) + if scale_magnitude: + vel = vel*scale + return vel + + def copy_scaled(self, centered_shape, scale_magnitude=True, as_var=None, device=None, var_name=None, trainable=None): + if as_var is None: + as_var = self.is_var + if as_var and var_name is None: + var_name = self._name + '_scaled' + if trainable is None: + trainable = self._is_trainable + x_scaled, y_scaled, z_scaled = self.scaled(centered_shape, scale_magnitude) + grid = VelocityGrid(centered_shape, x=x_scaled, y=y_scaled, z=z_scaled, as_var=as_var, \ + boundary=self.boundary, scale_renderer=self.scale_renderer, warp_renderer=self.warp_renderer, device=device, var_name=var_name, trainable=trainable) + return grid + + def _lut_warp_vel(self, shape, dt=1.0): + # use to get lookup positions to warp velocity components + vel = self._sampled_to_shape(shape) #3 x 1DHW1 + #coords = get_coord_field(shape, lod=0.0, concat=False) #4 x 1DHW1 + #vel_lut = [coords[i] - vel[i]*dt for i in range(len(vel))] + [coords[-1]] #4 x 1DHW1 + vel_lut = [- vel[i]*dt for i in range(len(vel))] #3 x 1DHW1 + vel_lut = tf.concat(vel_lut, axis = -1) #1DHW3 + return vel_lut + + def _warp_vel_component(self, data, lut, order=1, dt=1.0, clamp="NONE"): + if order<1 or order>2: + raise ValueError("Unsupported warp order '{}'".format(order)) + warped = self.warp_renderer._sample_LuT(data, lut, True, relative=True) + clamp = clamp.upper() + if order==2: #MacCormack + warped_back = self.warp_renderer._sample_LuT(warped, -lut, True, relative=True) + corrected = warped + 0.5*(data-warped_back) + if clamp=="MC" or clamp=="MC_SMOOTH": + #raise NotImplementedError("MacCormack clamping has not been implemented.") + fm = self.warp_renderer.filter_mode + self.warp_renderer.filter_mode = "MIN" + data_min = self.warp_renderer._sample_LuT(data, lut, True, relative=True) + self.warp_renderer.filter_mode = "MAX" + data_max = self.warp_renderer._sample_LuT(data, lut, True, relative=True) + self.warp_renderer.filter_mode = fm + if clamp=='MC': + #LOG.warning("Experimental clamp for MacCormack velocity advection.") + raise NotImplementedError("MIM and MAX warp sampling have wrong gradients.") + corrected = tf.clip_by_value(corrected, data_min, data_max) + if clamp=='MC_SMOOTH': + #LOG.warning("Experimental 'revert' clamp for MacCormack velocity advection.") + clamp_OOB = tf.logical_or(tf.less(corrected, data_min), tf.greater(corrected, data_max)) + corrected = tf.where(clamp_OOB, warped, corrected) + warped = corrected + return warped + + def warped(self, vel_grid=None, order=1, dt=1.0, clamp="NONE"): + if vel_grid is None: + #vel_grid = self + pass + elif not isinstance(vel_grid, VelocityGrid): + raise TypeError("Invalid VelocityGrid") + with self.warp_renderer.profiler.sample("warp velocity"): + LOG.debug("Warping velocity grid") + #TODO will cause errors if grid shapes do not match, resample if necessary? + if vel_grid is None: + lut_x = tf.concat([-vel*dt for vel in self._sampled_to_component_shape('X', concat=False)], axis=-1) + else: + lut_x = vel_grid._lut_warp_vel(self.x_shape, dt) + x_warped = self._warp_vel_component(self.x, lut_x, order=order, dt=dt, clamp=clamp) + del lut_x + + if vel_grid is None: + lut_y = tf.concat([-vel*dt for vel in self._sampled_to_component_shape('Y', concat=False)], axis=-1) + else: + lut_y = vel_grid._lut_warp_vel(self.y_shape, dt) + y_warped = self._warp_vel_component(self.y, lut_y, order=order, dt=dt, clamp=clamp) + del lut_y + + + if vel_grid is None: + lut_z = tf.concat([-vel*dt for vel in self._sampled_to_component_shape('Z', concat=False)], axis=-1) + else: + lut_z = vel_grid._lut_warp_vel(self.z_shape, dt) + z_warped = self._warp_vel_component(self.z, lut_z, order=order, dt=dt, clamp=clamp) + del lut_z + ''' + lut_warp_x = vel_grid._lut_warp_vel(self.x_shape, dt) + lut_warp_y = vel_grid._lut_warp_vel(self.y_shape, dt) + lut_warp_z = vel_grid._lut_warp_vel(self.z_shape, dt) + x_warped = self.warp_renderer._sample_LuT(self.x, lut_warp_x, True, relative=True) + y_warped = self.warp_renderer._sample_LuT(self.y, lut_warp_y, True, relative=True) + z_warped = self.warp_renderer._sample_LuT(self.z, lut_warp_z, True, relative=True) + # x_warped = self.warp_renderer._sample_LuT(self.x, vel_grid._lut_warp_vel(self.x_shape, dt), True, relative=False) + # y_warped = self.warp_renderer._sample_LuT(self.y, vel_grid._lut_warp_vel(self.y_shape, dt), True, relative=False) + # z_warped = self.warp_renderer._sample_LuT(self.z, vel_grid._lut_warp_vel(self.z_shape, dt), True, relative=False) + clamp = clamp.upper() + if order==2: #MacCormack + #raise NotImplementedError + x_warped_back = self.warp_renderer._sample_LuT(x_warped, -lut_warp_x, True, relative=True) + x_warped += 0.5*(self.x-x_warped_back) + y_warped_back = self.warp_renderer._sample_LuT(y_warped, -lut_warp_y, True, relative=True) + y_warped += 0.5*(self.y-y_warped_back) + z_warped_back = self.warp_renderer._sample_LuT(z_warped, -lut_warp_z, True, relative=True) + z_warped += 0.5*(self.z-z_warped_back) + #clamp? + if clamp=="MC" or clamp=="MC_SMOOTH": + raise NotImplementedError() + elif order>2: + raise ValueError("Unsupported warp order '{}'".format(order)) + ''' + #VelocityGrid(self.centered_shape, as_var=False, x=x_warped, y=y_warped, z=z_warped, scale_renderer=scale_renderer, warp_renderer=warp_renderer, coords=self.coords, lod=self.lod_pad) + return x_warped, y_warped, z_warped + + def copy_warped(self, vel_grid=None, as_var=None, order=1, dt=1.0, device=None, var_name=None, clamp="NONE", trainable=None): + if as_var is None: + as_var = self.is_var + if as_var and var_name is None: + var_name = self._name + '_warped' + if trainable is None: + trainable = self._is_trainable + x_warped, y_warped, z_warped = self.warped(vel_grid, order, dt, clamp=clamp) + grid = VelocityGrid(self.centered_shape, x=x_warped, y=y_warped, z=z_warped, as_var=as_var, \ + boundary=self.boundary, scale_renderer=self.scale_renderer, warp_renderer=self.warp_renderer, device=device, var_name=var_name, trainable=trainable) + #grid.assign(x_warped, y_warped, z_warped) + return grid + + def divergence_free(self, residual=1e-5): + raise NotImplementedError + + def var_list(self): + if self.is_var: + return [self._x, self._y, self._z] + else: + raise TypeError("This VelocityGrid is not a variable.") + + def get_variables(self): + if self.is_var: + return {'velocity_x': self._x, 'velocity_y': self._y, 'velocity_z': self._z} + else: + raise TypeError("This VelocityGrid is not a variable.") + + def get_output_variables(self, centered=True, staggered=True, include_MS=False, include_residual=False, only_trainable=False): + return {'velocity_x': self._x, 'velocity_y': self._y, 'velocity_z': self._z} + + def save(self, path): + np.savez_compressed(path, centered_shape=self.centered_shape, vel_x=self.x.numpy(), vel_y=self.y.numpy(), vel_z=self.z.numpy()) + + #def load(self, path): + # vel = np.load(path) + # self.x.assign(vel["vel_x"]) + # self.y.assign(vel["vel_y"]) + # self.z.assign(vel["vel_z"]) + + def assign(self, x,y,z): + x_shape = shape_list(x) + if not len(x_shape)==5 or not x_shape[-1]==1 or not x_shape[-4:-1]==self.x_shape: + raise ValueError("Invalid or incompatible shape of velocity x component on assignment: is {}, required: NDHW1 with DHW={}".format(x_shape, self.x_shape)) + y_shape = shape_list(y) + if not len(y_shape)==5 or not y_shape[-1]==1 or not y_shape[-4:-1]==self.y_shape: + raise ValueError("Invalid or incompatible shape of velocity y component on assignment: is {}, required: NDHW1 with DHW={}".format(y_shape, self.y_shape)) + z_shape = shape_list(z) + if not len(z_shape)==5 or not z_shape[-1]==1 or not z_shape[-4:-1]==self.z_shape: + raise ValueError("Invalid or incompatible shape of velocity z component on assignment: is {}, required: NDHW1 with DHW={}".format(z_shape, self.z_shape)) + if self.is_var: + self._x.assign(x) + self._y.assign(y) + self._z.assign(z) + else: + with tf.device(self._device): + self._x = tf.identity(x) + self._y = tf.identity(y) + self._z = tf.identity(z) + + def assign_centered(self, centered_grid): + assert isinstance(centered_grid, (np.ndarray, tf.Tensor)) + centered_shape = shape_list(centered_grid) + if not len(centered_shape)==5 or not centered_shape[-1]==3 or not centered_shape[-4:-1]==self.centered_shape: + raise ValueError("Invalid or incompatible shape of centered velocity assignment: is {}, required: NDHW3 with DHW={}".format(centered_shape, self.centered_shape)) + + x,y,z = self._centered_to_staggered(centered_grid) + if self.is_var: + self._x.assign(x) + self._y.assign(y) + self._z.assign(z) + else: + with tf.device(self._device): + self._x = tf.identity(x) + self._y = tf.identity(y) + self._z = tf.identity(z) + + def assign_staggered_combined(self, staggered_grid): + # from a combined staggered grid where all dimensions are centered+1 + assert isinstance(staggered_grid, (np.ndarray, tf.Tensor)) + x,y,z = tf.split(staggered_grid, 3, axis=-1) + x = x[...,:-1,:-1,:,:] + y = y[...,:-1,:,:-1,:] + z = z[...,:,:-1,:-1,:] + self.assign(x,y,z) + + def assign_centered_scaled(self, centered_grid, scale_magnitude=True): + assert isinstance(centered_grid, (np.ndarray, tf.Tensor)) + centered_shape = shape_list(centered_grid) + if not len(centered_shape)==5 or not centered_shape[-1]==3: + raise ValueError("Invalid or incompatible shape of centered velocity assignment: is {}, required: NDHW3 with DHW={}".format(centered_shape, self.centered_shape)) + + x,y,z = self._centered_to_staggered(centered_grid, output_centered_shape=self.centered_shape, scale_magnitude=scale_magnitude) + if self.is_var: + self._x.assign(x) + self._y.assign(y) + self._z.assign(z) + else: + with tf.device(self._device): + self._x = tf.identity(x) + self._y = tf.identity(y) + self._z = tf.identity(z) + + def assign_staggered_combined_scaled(self, staggered_grid): + # TODO + self.assign_staggered_combined(staggered_grid) + + def assign_add(self, x,y,z): + x_shape = shape_list(x) + if not len(x_shape)==5 or not x_shape[-1]==1 or not x_shape[-4:-1]==self.x_shape: + raise ValueError("Invalid or incompatible shape of velocity x component on assignment: is {}, required: NDHW1 with DHW={}".format(x_shape, self.x_shape)) + y_shape = shape_list(y) + if not len(y_shape)==5 or not y_shape[-1]==1 or not y_shape[-4:-1]==self.y_shape: + raise ValueError("Invalid or incompatible shape of velocity y component on assignment: is {}, required: NDHW1 with DHW={}".format(y_shape, self.y_shape)) + z_shape = shape_list(z) + if not len(z_shape)==5 or not z_shape[-1]==1 or not z_shape[-4:-1]==self.z_shape: + raise ValueError("Invalid or incompatible shape of velocity z component on assignment: is {}, required: NDHW1 with DHW={}".format(z_shape, self.z_shape)) + if self.is_var: + self._x.assign_add(x) + self._y.assign_add(y) + self._z.assign_add(z) + else: + with tf.device(self._device): + self._x = tf.identity(self._x+x) + self._y = tf.identity(self._y+y) + self._z = tf.identity(self._z+z) + + def assign_sub(self, x,y,z): + x_shape = shape_list(x) + if not len(x_shape)==5 or not x_shape[-1]==1 or not x_shape[-4:-1]==self.x_shape: + raise ValueError("Invalid or incompatible shape of velocity x component on assignment: is {}, required: NDHW1 with DHW={}".format(x_shape, self.x_shape)) + y_shape = shape_list(y) + if not len(y_shape)==5 or not y_shape[-1]==1 or not y_shape[-4:-1]==self.y_shape: + raise ValueError("Invalid or incompatible shape of velocity y component on assignment: is {}, required: NDHW1 with DHW={}".format(y_shape, self.y_shape)) + z_shape = shape_list(z) + if not len(z_shape)==5 or not z_shape[-1]==1 or not z_shape[-4:-1]==self.z_shape: + raise ValueError("Invalid or incompatible shape of velocity z component on assignment: is {}, required: NDHW1 with DHW={}".format(z_shape, self.z_shape)) + if self.is_var: + self._x.assign_sub(x) + self._y.assign_sub(y) + self._z.assign_sub(z) + else: + with tf.device(self._device): + self._x = tf.identity(self._x-x) + self._y = tf.identity(self._y-y) + self._z = tf.identity(self._z-z) + + def scale_magnitude(self, scale): + if np.isscalar(scale): + scale = [scale]*3 + assert len(scale)==3 + self.assign(self.x*scale[0],self.y*scale[1], self.z*scale[2]) + + def _centered_to_staggered_old(self, centered): + centered_shape = shape_list(centered) + assert len(centered_shape)==5 + assert centered_shape[-1]==3 + #assert centered_shape[0]==1 + batch_size = centered_shape[0] + assert self.centered_shape==centered_shape[-4:-1] + with self.scale_renderer.profiler.sample("centered velocity to staggered"): + x,y,z= tf.split(centered, 3, axis=-1) + # TODO: rework to use Renderer.resample_grid3D_aligned() + centered_x_transform = GridTransform(self.centered_shape, scale=[2./_ for _ in self.x_shape[::-1]], center=True) + centered_y_transform = GridTransform(self.centered_shape, scale=[2./_ for _ in self.y_shape[::-1]], center=True) + centered_z_transform = GridTransform(self.centered_shape, scale=[2./_ for _ in self.z_shape[::-1]], center=True) + # only shape important here + staggered_x_transform = GridTransform(self.x_shape)#,translation=[0.5,0,0]) + staggered_y_transform = GridTransform(self.y_shape)#,translation=[0,0.5,0]) + staggered_z_transform = GridTransform(self.z_shape)#,translation=[0,0,0.5]) + x = tf.squeeze(self.scale_renderer._sample_transform(x, [centered_x_transform]*batch_size, [staggered_x_transform]),1) + y = tf.squeeze(self.scale_renderer._sample_transform(y, [centered_y_transform]*batch_size, [staggered_y_transform]),1) + z = tf.squeeze(self.scale_renderer._sample_transform(z, [centered_z_transform]*batch_size, [staggered_z_transform]),1) + return x,y,z + + def _centered_to_staggered(self, centered, output_centered_shape=None, scale_magnitude=True): + centered_shape = shape_list(centered) + assert len(centered_shape)==5 #NDHWC + assert centered_shape[-1]==3 #C=3 + #assert centered_shape[0]==1 + batch_size = centered_shape[0] + if output_centered_shape is None: + output_centered_shape = centered_shape[-4:-1] + scale_factors = None + else: + assert has_shape(output_centered_shape, [3]) + scale_factors = [o/i for o,i in zip(output_centered_shape, centered_shape[-4:-1])] #shape DHW -> zyx + #assert self.centered_shape==centered_shape[-4:-1] + with self.scale_renderer.profiler.sample("centered velocity to staggered"): + x_shape, y_shape, z_shape = self.component_shapes(output_centered_shape) + x,y,z= tf.split(centered, 3, axis=-1) + x = self.scale_renderer.resample_grid3D_aligned(x, x_shape, align_x="CENTER", align_y="BORDER", align_z="BORDER") + y = self.scale_renderer.resample_grid3D_aligned(y, y_shape, align_x="BORDER", align_y="CENTER", align_z="BORDER") + z = self.scale_renderer.resample_grid3D_aligned(z, z_shape, align_x="BORDER", align_y="BORDER", align_z="CENTER") + if scale_magnitude and scale_factors is not None: + x = x*scale_factors[2] + y = y*scale_factors[1] + z = z*scale_factors[0] + return x,y,z + + def _scalar_centered_to_staggered(self, centered, *, allow_split_channels=False): + centered_shape = shape_list(centered) + assert len(centered_shape)==5 + assert (allow_split_channels or centered_shape[-1] in [1,2,4]), "resampling only supports 1,2 or 4 channels." + batch_size = centered_shape[0] + staggered_shape = self.shape_centered_to_staggered(centered_shape[-4:-1]) + with self.scale_renderer.profiler.sample("centered scalar to staggered"): + if (allow_split_channels and centered_shape[-1] not in [1,2,4]): + splits = tf_split_to_size(centered, [1,2,4], axis=-1) + staggered = [] + for split in splits: + staggered.append(self.scale_renderer.resample_grid3D_aligned(split, staggered_shape, align_x='STAGGER_OUTPUT', align_y='STAGGER_OUTPUT', align_z='STAGGER_OUTPUT')) + staggered = tf.concat(staggered, axis=-1) + assert shape_list(staggered)[-1]==centered_shape[-1] + else: + staggered = self.scale_renderer.resample_grid3D_aligned(centered, staggered_shape, align_x='STAGGER_OUTPUT', align_y='STAGGER_OUTPUT', align_z='STAGGER_OUTPUT') + return staggered + + def _staggeredTensor_to_components(self, tensor, reverse=False): + tensor_shape = GridShape.from_tensor(tensor) + # assert len(tensor_shape)==5 + assert tensor_shape.c==3 + #assert tensor_shape.n==1 + #assert np.asarray(self.centered_shape)+np.asarray([1,1,1])== tensor_shape.xyz.as_shape() #tensor_shape[-4:-1] + tensor = tensor_shape.normalize_tensor_shape(tensor) + components = tf.split(tensor, 3, axis=-1) + if reverse: + components = components[::-1] + x = components[0][:,:-1,:-1,:] + y = components[1][:,:-1,:,:-1] + z = components[2][:,:,:-1,:-1] + return x,y,z + + def _components_to_staggeredTensor(self, comp_x,comp_y,comp_z, reverse=False): + z = (0,0) + p = (0,1) + components = [ + tf.pad(comp_x, [z,p,p,z,z]), + tf.pad(comp_y, [z,p,z,p,z]), + tf.pad(comp_z, [z,z,p,p,z]), + ] + if reverse: + components = components[::-1] + return tf.concat(components, axis=-1) + + def as_staggeredTensor(self, reverse=False): + return self._components_to_staggeredTensor(self.x, self.y, self.z, reverse=reverse) + + def _sampled_to_shape(self, shape): + with self.scale_renderer.profiler.sample("velocity to shape"): + # uniform scaling, centered grids + #_sample_transform is currently experimental and assumes the output grid to be in a centered [-1,1] cube, so scale input accordingly + # scale with output shape to get the right 0.5 offset + scale = [2./_ for _ in shape[::-1]] + staggered_x_transform = GridTransform(self.x_shape, scale=scale, center=True) + staggered_y_transform = GridTransform(self.y_shape, scale=scale, center=True) + staggered_z_transform = GridTransform(self.z_shape, scale=scale, center=True) + # only shape important here + sample_transform = GridTransform(shape) + #check if shape matches component shape to avoid sampling (e.g. for self warping) + vel_sampled = [ + tf.squeeze(self.scale_renderer._sample_transform(self.x, [staggered_x_transform], [sample_transform]),1) \ + if not shape==self.x_shape else tf.identity(self.x), #1DHW1 + tf.squeeze(self.scale_renderer._sample_transform(self.y, [staggered_y_transform], [sample_transform]),1) \ + if not shape==self.y_shape else tf.identity(self.y), + tf.squeeze(self.scale_renderer._sample_transform(self.z, [staggered_z_transform], [sample_transform]),1) \ + if not shape==self.z_shape else tf.identity(self.z), + ] + return vel_sampled + + def _staggered_to_centered(self, staggered, concat=True): + staggered_shape = shape_list(staggered) + assert len(staggered_shape) == 5 + assert staggered_shape[-1] == 3 + with self.warp_renderer.profiler.sample("staggered_to_centered"): + h = tf.constant(0.5, dtype=tf.float32) + x,y,z = tf.split(staggered, 3, axis=-1) + # x = components[0][:,:-1,:-1,:] + # y = components[1][:,:-1,:,:-1] + # z = components[2][:,:,:-1,:-1] + vel_centered = [ + (x[:,:-1,:-1,1:] + x[:,:-1,:-1,:-1])*h, + (y[:,:-1,1:,:-1] + y[:,:-1,:-1,:-1])*h, + (z[:,1:,:-1,:-1] + z[:,:-1,:-1,:-1])*h, + ] + if concat: + vel_centered = tf.concat(vel_centered, axis=-1) + return vel_centered + + + @staticmethod + def _check_vel_component_shape(x,y,z): + # staggered velocity: shape(z,y,x) + # x: (+0,+0,+1) + # y: (+0,+1,+0) + # z: (+1,+0,+0) + x_shape = shape_list(x) + if len(x_shape)!=5 or x_shape[-1]!=1: return False + c_shape = copy.copy(x_shape[-4:-1]) + c_shape[2] -=1 + batch = x_shape[-1] + + y_shape = shape_list(y) + if len(y_shape)!=5 or y_shape[-1]!=1: return False + if y_shape[-4]!=c_shape[0] or y_shape[-3]!=(c_shape[1]+1) or y_shape[-2]!=c_shape[2]: return False + if y_shape[-1]!=batch: return False + + z_shape = shape_list(z) + if len(z_shape)!=5 or z_shape[-1]!=1: return False + if z_shape[-4]!=(c_shape[0]+1) or z_shape[-3]!=c_shape[1] or z_shape[-2]!=c_shape[2]: return False + if z_shape[-1]!=batch: return False + + return True + + @staticmethod + def _check_potential_component_shape(x,y,z): + # staggered potential: shape(z,y,x) + # x: (+1,+1,+0) + # y: (+1,+0,+1) + # z: (+0,+1,+1) + x_shape = shape_list(x) + if len(x_shape)!=5 or x_shape[-1]!=1: return False + c_shape = copy.copy(x_shape[-4:-1]) #z,y,x + c_shape[0] -=1 + c_shape[1] -=1 + batch = x_shape[-1] + + y_shape = shape_list(y) + if len(y_shape)!=5 or y_shape[-1]!=1: return False + if y_shape[-4]!=(c_shape[0]+1) or y_shape[-3]!=(c_shape[1]) or y_shape[-2]!=(c_shape[2]+1): return False + if y_shape[-1]!=batch: return False + + z_shape = shape_list(z) + if len(z_shape)!=5 or z_shape[-1]!=1: return False + if z_shape[-4]!=(c_shape[0]) or z_shape[-3]!=(c_shape[1]+1) or z_shape[-2]!=(c_shape[2]+1): return False + if z_shape[-1]!=batch: return False + + return True + + def _components_to_centered(self, x,y,z, concat=True): + if not self._check_vel_component_shape(x,y,z): raise ValueError("shapes of components do not fit. x: {}, y: {}, z: {}".format(shape_list(x), shape_list(y), shape_list(z))) + with self.warp_renderer.profiler.sample("components_to_centered"): + h = tf.constant(0.5, dtype=tf.float32) + vel_centered = [ + (x[:,:,:,1:] + x[:,:,:,:-1])*h, + (y[:,:,1:] + y[:,:,:-1])*h, + (z[:,1:] + z[:,:-1])*h, + ] + if concat: + vel_centered = tf.concat(vel_centered, axis=-1) + return vel_centered + + + def _staggered_components_potential_to_staggered_components(self, pot_x, pot_y, pot_z): + if not self._check_potential_component_shape(pot_x, pot_y, pot_z): + raise ValueError("shapes of potential components do not fit. x: {}, y: {}, z: {}".format(shape_list(pot_x), shape_list(pot_y), shape_list(pot_z))) + + vel_components = [ + (pot_z[:,:,:-1,:,:] - pot_z[:,:,1:,:,:]) - (pot_y[:,:-1,:,:,:] - pot_y[:,1:,:,:,:]), # dPz/dy - dPy/dz + (pot_x[:,:-1,:,:,:] - pot_x[:,1:,:,:,:]) - (pot_z[:,:,:,:-1,:] - pot_z[:,:,:,1:,:]), # dPx/dz - dPz/dx + (pot_y[:,:,:,:-1,:] - pot_y[:,:,:,1:,:]) - (pot_x[:,:,:-1,:,:] - pot_x[:,:,1:,:,:]), # dPy/dx - dPx/dy + ] + + return tuple(vel_components) + + def _staggeredTensor_potential_to_staggered_components(self, pot): + tensor_shape = GridShape.from_tensor(pot) + assert tensor_shape.c==3 + pot = tensor_shape.normalize_tensor_shape(pot) + pot_x, pot_y, pot_z = tf.split(pot, 3, axis=-1) + + vel_components = [ + (pot_z[:,:-1,:-1,:,:] - pot_z[:,:-1,1:,:,:]) - (pot_y[:,:-1,:-1,:,:] - pot_y[:,1:,:-1,:,:]), # dPz/dy - dPy/dz + (pot_x[:,:-1,:,:-1,:] - pot_x[:,1:,:,:-1,:]) - (pot_z[:,:-1,:,:-1,:] - pot_z[:,:-1,:,1:,:]), # dPx/dz - dPz/dx + (pot_y[:,:,:-1,:-1,:] - pot_y[:,:,:-1,1:,:]) - (pot_x[:,:,:-1,:-1,:] - pot_x[:,:,1:,:-1,:]), # dPy/dx - dPx/dy + ] + + return tuple(vel_components) + + def _staggeredTensor_potential_to_components(self, tensor, reverse=False): + tensor_shape = GridShape.from_tensor(tensor) + # assert len(tensor_shape)==5 + assert tensor_shape.c==3 + #assert tensor_shape.n==1 + #assert np.asarray(self.centered_shape)+np.asarray([1,1,1])== tensor_shape.xyz.as_shape() #tensor_shape[-4:-1] + tensor = tensor_shape.normalize_tensor_shape(tensor) + components = tf.split(tensor, 3, axis=-1) + if reverse: + components = components[::-1] + x = components[0][:,:,:,:-1,:] + y = components[1][:,:,:-1,:,:] + z = components[2][:,:-1,:,:,:] + return x,y,z + + def _components_potential_to_staggeredTensor_potential(self, comp_x, comp_y, comp_z, reverse=False): + z = (0,0) + p = (0,1) + components = [ + tf.pad(comp_x, [z,z,z,p,z]), + tf.pad(comp_y, [z,z,p,z,z]), + tf.pad(comp_z, [z,p,z,z,z]), + ] + if reverse: + components = components[::-1] + return tf.concat(components, axis=-1) + + def _staggeredTensor_potential_to_staggeredTensor(self, pot): + return self._components_to_staggeredTensor(*self._staggeredTensor_potential_to_staggered_components(pot)) + + def _staggeredTensor_potential_to_centered(self, pot): + return self._components_to_centered(*self._staggeredTensor_potential_to_staggered_components(pot)) + + def _centered_to_curl(self, vel): + raise NotImplementedError("Deprecated") + assert isinstance(vel, (tf.Tensor, np.ndarray)) + vel_shape = shape_list(vel) + assert len(vel_shape)==5 and vel_shape[-1]==3 #NDHWC + + # https://en.wikipedia.org/wiki/Curl_(mathematics) + # + with self.warp_renderer.profiler.sample("centered_to_curl"): + #vel = tf.pad(vel, [(0,0),(1,1),(1,1),(1,1),(0,0)], "SYMMETRIC") + vel_x, vel_y, vel_z = tf.split(vel, 3, axis=-1) + # curl_x = d vel_z/d y - d vel_y/d z + # d vel_z/d y -> finite (central) difference + + def central_diff(v, axis): + pad = [(0,0)]*5 + pad[axis] = (1,1) + v = tf.pad(v, pad, "SYMMETRIC") + + slice_size = shape_list(v) + slice_size[axis] -= 2 + + slice_begin = [0]*5 + v_a = tf.slice(v, begin=slice_begin, size=slice_size) + + slice_begin[axis] = 2 + v_b = tf.slice(v, begin=slice_begin, size=slice_size) + + return v_a - v_b + + # vel_x_dy = central_diff(vel_x, axis=-3) #vel_x[:,:,:-2,:,:] - vel_x[:,:,2:,:,:] + # vel_x_dz = central_diff(vel_x, axis=-4) #vel_x[:,:-2,:,:,:] - vel_x[:,2:,:,:,:] + # vel_y_dx = central_diff(vel_y, axis=-2) #vel_y[:,:,:,:-2,:] - vel_y[:,:,:,2:,:] + # vel_y_dz = central_diff(vel_y, axis=-4) #vel_y[:,:-2,:,:,:] - vel_y[:,2:,:,:,:] + # vel_z_dx = central_diff(vel_z, axis=-2) #vel_z[:,:,:,:-2,:] - vel_z[:,:,:,2:,:] + # vel_z_dy = central_diff(vel_z, axis=-3) #vel_z[:,:,:-2,:,:] - vel_z[:,:,2:,:,:] + + # curl_x = vel_z_dy - vel_y_dz + # curl_y = vel_x_dz - vel_z_dx + # curl_z = vel_y_dx - vel_x_dy + + curl = [ + central_diff(vel_z, axis=-3) - central_diff(vel_y, axis=-4), + central_diff(vel_x, axis=-4) - central_diff(vel_z, axis=-2), + central_diff(vel_y, axis=-2) - central_diff(vel_x, axis=-3), + ] + + return tf.concat(curl, axis=-1) + + + def _staggered_to_curl(self, x,y,z): + if not self._check_vel_component_shape(x,y,z): raise ValueError("shapes of components do not fit. x: {}, y: {}, z: {}".format(shape_list(x), shape_list(y), shape_list(z))) + with self.warp_renderer.profiler.sample("staggered_to_curl"): + pass + raise NotImplementedError + + def centered(self, pad_lod=False, concat=True):#, shape=None): + # if shape is None: + shape = self.centered_shape + with self.warp_renderer.profiler.sample("velocity to centered"): + #vel_centered = self._sampled_to_shape(shape)#3 x 1DHW1 + h = tf.constant(0.5, dtype=tf.float32) + vel_centered = [ + (self.x[:,:,:,1:] + self.x[:,:,:,:-1])*h, + (self.y[:,:,1:] + self.y[:,:,:-1])*h, + (self.z[:,1:] + self.z[:,:-1])*h, + ] + if pad_lod: + vel_centered.append(self.lod_pad)#4 x 1DHW1 + if concat: + vel_centered = tf.concat(vel_centered, axis=-1) #1DHW[3|4] + # vel_centered = [tf.squeeze(_, -1) for _ in vel_centered] #3 x 1DHW + # vel_centered = tf.transpose(vel_centered, (1,2,3,4,0))#1DHW3 + # if pad_lod: + # vel_centered = tf.concat([vel_centered, self.lod_pad], axis = -1) + return vel_centered + + def _sampled_to_component_shape(self, component, pad_lod=False, concat=True): + # grids have the same spacing/resolution, so global/constant offset + component = component.upper() + offset_coord_from = 0.5 + offset_coord_to = -0.5 + with self.warp_renderer.profiler.sample("velocity to component shape"): + vel_sampled = [] + # sample x + vel_sampled.append(tf.identity(self.x) if component=='X' else \ + tf.squeeze(self.warp_renderer.resample_grid3D_offset(self.x, \ + offsets = [[offset_coord_from,offset_coord_to,0.0] if component=='Y' else [offset_coord_from,0.0,offset_coord_to],], \ + target_shape = self.y_shape if component=='Y' else self.z_shape), 1)) + # sample y + vel_sampled.append(tf.identity(self.y) if component=='Y' else \ + tf.squeeze(self.warp_renderer.resample_grid3D_offset(self.y, \ + offsets = [[offset_coord_to,offset_coord_from,0.0] if component=='X' else [0.0,offset_coord_from,offset_coord_to],], \ + target_shape = self.x_shape if component=='X' else self.z_shape), 1)) + # sample z + vel_sampled.append(tf.identity(self.z) if component=='Z' else \ + tf.squeeze(self.warp_renderer.resample_grid3D_offset(self.z, \ + offsets = [[offset_coord_to,0.0,offset_coord_from] if component=='X' else [0.0,offset_coord_to,offset_coord_from],], \ + target_shape = self.x_shape if component=='X' else self.y_shape), 1)) + + if pad_lod: + vel_sampled.append(self.lod_pad)#4 x 1DHW1 + if concat: + vel_sampled = tf.concat(vel_sampled, axis=-1) #1DHW[3|4] + return vel_sampled + + def centered_lut_grid(self, dt=1.0, centered_velocity=None): + vel_centered = self.centered() if (centered_velocity is None) else centered_velocity + #vel_lut = tf.concat([self.coords - vel_centered * dt, self.lod_pad], axis = -1) + vel_lut = vel_centered * (- dt) + return vel_lut + + def warp(self, data, order=1, dt=1.0, clamp="NONE", centered_velocity=None): + with self.warp_renderer.profiler.sample("warp scalar"): + v = self.centered_lut_grid(dt=dt, centered_velocity=centered_velocity) + data_shape = spacial_shape_list(data) + vel_shape = spacial_shape_list(v) + if data_shape!=vel_shape: #self.centered_shape: + raise ValueError("Shape mismatch in centered warp: data {}, velocity {}".format(data_shape, vel_shape)) + # LOG.debug("Scaling velocity grid from %s to %s for warping", self.centered_shape, data_shape) + #TODO handle vector mipmapping and filtering... + # v = self.scale_renderer.resample_grid3D_aligned(v, data_shape) + # vel_scale = [o/i for o,i in zip(data_shape, self.centered_shape)]#z,y,x + # LOG.debug("Rescale velocity for warping from %s to %s with vector scale %s", self.centered_shape, data_shape, vel_scale) + # vel_scale = tf.constant(vel_scale[::-1] + [0.], dtype=tf.float32) #z,y,x -> x,y,z,lod(=0) + #TODO this is wrong, v is already a LuT (absolute positions) + # v = v * vel_scale + LOG.debug("Warping density grid") + data_warped = self.warp_renderer._sample_LuT(data, v, True, relative=True) + + clamp = clamp.upper() + if order==2: #MacCormack + #raise NotImplementedError + data_warped_back = self.warp_renderer._sample_LuT(data_warped, -v, True, relative=True) + #data_warped_back = self.warp(data_warped, dt=-dt) #self.warp_renderer._sample_LuT(data, v, True) + data_corr = data_warped + 0.5*(data-data_warped_back) + if clamp=='MC' or clamp=='MC_SMOOTH': #smooth clamp + #raise NotImplementedError("MacCormack clamping has not been implemented.") + fm = self.warp_renderer.filter_mode + self.warp_renderer.filter_mode = "MIN" + data_min = self.warp_renderer._sample_LuT(data, v, True, relative=True) + self.warp_renderer.filter_mode = "MAX" + data_max = self.warp_renderer._sample_LuT(data, v, True, relative=True) + self.warp_renderer.filter_mode = fm + if clamp=='MC': + #LOG.warning("Experimental clamp for MacCormack density advection.") + raise NotImplementedError("MIM and MAX warp sampling have wrong gradients.") + data_corr = tf.clip_by_value(data_corr, data_min, data_max) + if clamp=='MC_SMOOTH': + #LOG.warning("Experimental 'revert' clamp for MacCormack density advection.") + clamp_OOB = tf.logical_or(tf.less(data_corr, data_min), tf.greater(data_corr, data_max)) + data_corr = tf.where(clamp_OOB, data_warped, data_corr) + data_warped = data_corr + elif order>2: + raise ValueError("Unsupported warp order '{}'".format(order)) + + if clamp=='NEGATIVE': + data_warped = tf.maximum(data_warped, 0) + + return data_warped + + def with_buoyancy(self, value, scale_grid): + # value: [x,y,z] + # scale_grid: density 1DHW1 + if isinstance(scale_grid, DensityGrid): + scale_grid = scale_grid.with_inflow() #.d + assert len(shape_list(value))==1 + if not isinstance(value, (tf.Tensor, tf.Variable)): + value = tf.constant(value, dtype=tf.float32) + value = tf.reshape(value, [1,1,1,1,shape_list(value)[0]]) + buoyancy = value*scale_grid # 1DHW3 + return self + buoyancy + + """ + def apply_buoyancy(self, value, scale_grid): + # value: [x,y,z] + # scale_grid: density 1DHW1 + assert len(shape_list(value))==1 + value = tf.reshape(tf.constant(value, dtype=tf.float32), [1,1,1,1,shape_list(value)[0]]) + buoyancy = value*scale_grid # 1DHW3 + self += buoyancy + """ + #centered + def divergence(self, world_scale=[1,1,1]): + #out - in per cell, per axis + x_div = self.x[:,:,:,1:,:] - self.x[:,:,:,:-1,:] + y_div = self.y[:,:,1:,:,:] - self.y[:,:,:-1,:,:] + z_div = self.z[:,1:,:,:,:] - self.z[:,:-1,:,:,:] + # sum to get total divergence per cell + div = x_div*world_scale[0]+y_div*world_scale[1]+z_div*world_scale[2] + return div + #centered + def magnitude(self, world_scale=[1,1,1]): + with self.warp_renderer.profiler.sample("magnitude"): + v = self.centered(pad_lod=False)*tf.constant(world_scale, dtype=tf.float32) + return tf_norm2(v, axis=-1, keepdims=True) #tf.norm(v, axis=-1, keepdims=True) + + def stats(self, world_scale=[1,1,1], mask=None, state=None, **warp_kwargs): + ''' + mask: optional binary float mask, stats only consider cells>0.5 + ''' + x = self.x + if mask is not None: + mask_x = tf.greater(self.scale_renderer.resample_grid3D_aligned(mask, self.x_shape, align_x='stagger_output'), 0.5) + x = tf.boolean_mask(x, mask_x) + y = self.y + if mask is not None: + mask_y = tf.greater(self.scale_renderer.resample_grid3D_aligned(mask, self.y_shape, align_y='stagger_output'), 0.5) + y = tf.boolean_mask(y, mask_y) + z = self.z + if mask is not None: + mask_z = tf.greater(self.scale_renderer.resample_grid3D_aligned(mask, self.z_shape, align_z='stagger_output'), 0.5) + z = tf.boolean_mask(z, mask_z) + if mask is not None and mask.dtype!=tf.bool: + mask = tf.greater(mask, 0.5) + + divergence = self.divergence(world_scale) + if mask is not None: divergence = tf.boolean_mask(divergence, mask) + magnitude = self.magnitude(world_scale) + if mask is not None: magnitude = tf.boolean_mask(magnitude, mask) + + stats = { + # 'divMean':tf.reduce_mean(divergence), 'divMax':tf.reduce_max(divergence), 'divMin':tf.reduce_min(divergence), 'divAbsMean':tf.reduce_mean(tf.abs(divergence)), + # 'magMean':tf.reduce_mean(magnitude), 'magMax':tf.reduce_max(magnitude), 'magMin':tf.reduce_min(magnitude), + # 'xMean':tf.reduce_mean(x), 'xMax':tf.reduce_max(x), 'xMin':tf.reduce_min(x), 'xAbsMean':tf.reduce_mean(tf.abs(x)), + # 'yMean':tf.reduce_mean(y), 'yMax':tf.reduce_max(y), 'yMin':tf.reduce_min(y), 'yAbsMean':tf.reduce_mean(tf.abs(y)), + # 'zMean':tf.reduce_mean(z), 'zMax':tf.reduce_max(z), 'zMin':tf.reduce_min(z), 'zAbsMean':tf.reduce_mean(tf.abs(z)), + 'divergence': tf_tensor_stats(divergence, as_dict=True), + 'magnitude': tf_tensor_stats(magnitude, as_dict=True), + 'velocity_x': tf_tensor_stats(x, as_dict=True), + 'velocity_y': tf_tensor_stats(y, as_dict=True), + 'velocity_z': tf_tensor_stats(z, as_dict=True), + 'shape':self.centered_shape, 'bounds':self.outer_bounds, + } + + if state is not None and state.prev is not None and state.prev.velocity is not None: + prev_warped = state.prev.velocity_advected(**warp_kwargs) + + def vel_warp_SE_stats(prev, curr, mask): + warp_SE = tf.squared_difference(prev, curr) + if mask is not None: + warp_SE = tf.boolean_mask(warp_SE, mask) + return tf_tensor_stats(warp_SE, as_dict=True) + stats["warp_x_SE"] = vel_warp_SE_stats(prev_warped.x, self.x, mask_x if mask is not None else None) + stats["warp_y_SE"] = vel_warp_SE_stats(prev_warped.y, self.y, mask_y if mask is not None else None) + stats["warp_z_SE"] = vel_warp_SE_stats(prev_warped.z, self.z, mask_z if mask is not None else None) + + warp_vdiff_mag = (prev_warped-self).magnitude() + if mask is not None: + warp_vdiff_mag = tf.boolean_mask(warp_vdiff_mag, mask) + stats["warp_vdiff_mag"] = tf_tensor_stats(warp_vdiff_mag, as_dict=True) + del warp_vdiff_mag + + vel_CangleRad_mask = tf.greater(state.prev.velocity.magnitude() * self.magnitude(), 1e-8) + if mask is not None: + vel_CangleRad_mask = tf.logical_and(mask, vel_CangleRad_mask) + warp_CangleRad = tf_angle_between(state.prev.velocity.centered(), self.centered(), axis=-1, keepdims=True) + stats["warp_angleCM_rad"] = tf_tensor_stats(tf.boolean_mask(warp_CangleRad, vel_CangleRad_mask), as_dict=True) + del warp_CangleRad + + else: + stats["warp_x_SE"] = tf_tensor_stats(tf.zeros([1,1,1,1,1], dtype=tf.float32), as_dict=True) + stats["warp_y_SE"] = tf_tensor_stats(tf.zeros([1,1,1,1,1], dtype=tf.float32), as_dict=True) + stats["warp_z_SE"] = tf_tensor_stats(tf.zeros([1,1,1,1,1], dtype=tf.float32), as_dict=True) + stats["warp_vdiff_mag"] = tf_tensor_stats(tf.zeros([1,1,1,1,1], dtype=tf.float32), as_dict=True) + stats["warp_angleCM_rad"] = tf_tensor_stats(tf.zeros([1,1,1,1,1], dtype=tf.float32), as_dict=True) + + return stats + + def clear_cache(self): + pass + + def __add__(self, other): + if isinstance(other, VelocityGrid): + if self.centered_shape!=other.centered_shape: + raise ValueError("VelocityGrids of shape %s and %s are not compatible"%(self.centered_shape, other.centered_shape)) + return VelocityGrid(self.centered_shape, x=self.x+other.x, y=self.y+other.y, z=self.z+other.z, as_var=False, \ + boundary=self.boundary, scale_renderer=self.scale_renderer, warp_renderer=self.warp_renderer, device=None) + if isinstance(other, (np.ndarray, tf.Tensor, tf.Variable)): + other_shape = shape_list(other) + if self.centered_shape!=spacial_shape_list(other) or other_shape[0]!=1 or other_shape[-1]!=3: + raise ValueError("VelocityGrid of shape %s is not compatible with tensor of shape %s are not compatible"%(self.centered_shape, spacial_shape_list(other))) + x,y,z = self._centered_to_staggered(other) + return VelocityGrid(self.centered_shape, x=self.x+x, y=self.y+y, z=self.z+z, as_var=False, \ + boundary=self.boundary, scale_renderer=self.scale_renderer, warp_renderer=self.warp_renderer, device=None) + else: + return NotImplemented + + def __iadd__(self, other): + if isinstance(other, VelocityGrid): + if self.centered_shape!=other.centered_shape: + raise ValueError("VelocityGrids of shape %s and %s are not compatible"%(self.centered_shape, other.centered_shape)) + self.assign_add(other.x, other.y, other.z) + return self + if isinstance(other, (np.ndarray, tf.Tensor, tf.Variable)): + other_shape = shape_list(other) + if self.centered_shape!=spacial_shape_list(other) or other_shape[0]!=1 or other_shape[-1]!=3: + raise ValueError("VelocityGrid of shape %s is not compatible with tensor of shape %s are not compatible"%(self.centered_shape, spacial_shape_list(other))) + x,y,z = self._centered_to_staggered(other) + self.assign_add(x, y, z) + return self + else: + return NotImplemented + + def __sub__(self, other): + if isinstance(other, VelocityGrid): + if self.centered_shape!=other.centered_shape: + raise ValueError("VelocityGrids of shape %s and %s are not compatible"%(self.centered_shape, other.centered_shape)) + return VelocityGrid(self.centered_shape, x=self.x-other.x, y=self.y-other.y, z=self.z-other.z, as_var=False, \ + boundary=self.boundary, scale_renderer=self.scale_renderer, warp_renderer=self.warp_renderer, device=None) + if isinstance(other, (np.ndarray, tf.Tensor, tf.Variable)): + other_shape = shape_list(other) + if self.centered_shape!=spacial_shape_list(other) or other_shape[0]!=1 or other_shape[-1]!=3: + raise ValueError("VelocityGrid of shape %s is not compatible with tensor of shape %s are not compatible"%(self.centered_shape, spacial_shape_list(other))) + x,y,z = self._centered_to_staggered(other) + return VelocityGrid(self.centered_shape, x=self.x-x, y=self.y-y, z=self.z-z, as_var=False, \ + boundary=self.boundary, scale_renderer=self.scale_renderer, warp_renderer=self.warp_renderer, device=None) + else: + return NotImplemented + + def __isub__(self, other): + if isinstance(other, VelocityGrid): + if self.centered_shape!=other.centered_shape: + raise ValueError("VelocityGrids of shape %s and %s are not compatible"%(self.centered_shape, other.centered_shape)) + self.assign_sub(other.x, other.y, other.z) + return self + if isinstance(other, (np.ndarray, tf.Tensor, tf.Variable)): + other_shape = shape_list(other) + if self.centered_shape!=spacial_shape_list(other) or other_shape[0]!=1 or other_shape[-1]!=3: + raise ValueError("VelocityGrid of shape %s is not compatible with tensor of shape %s are not compatible"%(self.centered_shape, spacial_shape_list(other))) + x,y,z = self._centered_to_staggered(other) + self.assign_sub(x, y, z) + return self + else: + return NotImplemented + + @property + def is_centered(self): + return False + @property + def is_staggered(self): + return True + @property + def is_MS(self): + return False + @property + def has_MS_output(self): + return False + +class State: + def __init__(self, density, velocity, frame, prev=None, next=None, transform=None, targets=None, targets_raw=None, bkgs=None, masks=None): + self.density = density + self.velocity = velocity + + self.density_target = None + self.velocity_target = None + + self.density_proxy = None + + assert isinstance(frame, numbers.Integral) + self.frame = frame + assert is_None_or_type(prev, State) + self.prev = prev + assert is_None_or_type(next, State) + self.next = next + + assert isinstance(transform, Transform) + self.transform = transform + self.base_targets_raw = targets_raw + self.base_targets = targets + self.base_bkgs = bkgs + self.base_masks = masks + self.base_target_cameras = None + self.set_base_target_cameras_MS(None) + self.target_mask = None + self.images = None + self.t = None + self.SDF_positions = None + self._density_images_MS = {} + self._density_images_t_MS = {} + self._SDF_positions_MS = {} + + class StateIterator: + def __init__(self, state): + self.curr_state = state + def __next__(self): + if self.curr_state is not None: + state = self.curr_state + self.curr_state = state.next + return state + raise StopIteration + def __iter__(self): + return self.StateIterator(self) + + @property + def has_density(self): + return self.__density is not None + @property + def has_density_neural(self): + return self.has_density and False + @property + def density(self): + if self.has_density: + return self.__density + else: + raise AttributeError("State for frame {} does not contain density".format(self.frame)) + @density.setter + def density(self, value): + assert is_None_or_type(value, DensityGrid) + self.__density = value + @property + def _density(self): + raise AttributeError("State._density is deprecated. Use State.density".format(self.frame)) + @density.setter + def _density(self, value): + raise AttributeError("State._density is deprecated. Assign to State.density".format(self.frame)) + + @property + def has_velocity(self): + return self.__velocity is not None + @property + def velocity(self): + if self.has_velocity: + return self.__velocity + else: + raise AttributeError("State for frame {} does not contain velocity".format(self.frame)) + @velocity.setter + def velocity(self, value): + assert is_None_or_type(value, VelocityGrid) + self.__velocity = value + @property + def _velocity(self): + raise AttributeError("State._velocity is deprecated. Use State.velocity".format(self.frame)) + @velocity.setter + def _velocity(self, value): + raise AttributeError("State._velocity is deprecated. Assign to State.velocity".format(self.frame)) + + @property + def has_density_target(self): + return self.__density_target is not None + @property + def density_target(self): + if self.has_density_target: + return self.__density_target + else: + raise AttributeError("State for frame {} does not contain a density target".format(self.frame)) + @density_target.setter + def density_target(self, value): + assert is_None_or_type(value, DensityGrid) + self.__density_target = value + + @property + def has_density_proxy(self): + return self.__density_proxy is not None + @property + def density_proxy(self): + if self.has_density_proxy: + return self.__density_proxy + else: + raise AttributeError("State for frame {} does not contain a density proxy".format(self.frame)) + @density_proxy.setter + def density_proxy(self, value): + assert is_None_or_type(value, DensityGrid) + self.__density_proxy = value + + @property + def has_velocity_target(self): + return self.__velocity_target is not None + @property + def velocity_target(self): + if self.has_velocity_target: + return self.__velocity_target + else: + raise AttributeError("State for frame {} does not contain a velocity target".format(self.frame)) + @velocity_target.setter + def velocity_target(self, value): + assert is_None_or_type(value, VelocityGrid) + self.__velocity_target = value + + @property + def base_target_cameras(self): + if self.__target_cameras is not None: + return self.__target_cameras + else: + raise AttributeError("State: base_target_cameras not set.") + @base_target_cameras.setter + def base_target_cameras(self, value): + assert value is None or (isinstance(value, list) and all(isinstance(_, Camera) for _ in value)) + self.__target_cameras = value + @property + def target_cameras(self): + if self.target_mask is not None: + return [self.base_target_cameras[_] for _ in self.target_mask] + else: + return copy.copy(self.base_target_cameras) + @target_cameras.setter + def target_cameras(self, value): + raise AttributeError("Can't set target_cameras on State. Set base_target_cameras instead.") + + def base_target_cameras_MS(self, scale): + if self.__target_cameras_MS is not None: + return self.__target_cameras_MS[scale] + else: + raise AttributeError("State: base_target_cameras_MS not set.") + def set_base_target_cameras_MS(self, cameras): + assert cameras is None or (isinstance(cameras, dict) and all(isinstance(k, int) and \ + isinstance(v, list) and all(isinstance(c, Camera) for c in v) for k,v in cameras.items())), \ + "invalid MS cameras setup, must be dict of lists of cameras: {int: [Camera,]}" + self.__target_cameras_MS = copy.deepcopy(cameras) + def target_cameras_MS(self, scale): + if self.target_mask is not None: + return [self.base_target_cameras_MS(scale)[_] for _ in self.target_mask] + else: + return copy.copy(self.base_target_cameras_MS(scale)) + @property + def target_cameras_fwd_WS(self): + cams = self.target_cameras + pos = [cam.transform.forward_global()[:3] for cam in cams] + pos = - tf.constant(pos, dtype=tf.float32) #cam looks backwards? + pos = tf.reshape(pos, (1,len(cams),1,1,3)) #NVHWC + return pos + @property + def target_cameras_pos_WS(self): + cams = self.target_cameras + pos = [cam.transform.position_global()[:3] for cam in cams] + pos = tf.constant(pos, dtype=tf.float32) #cam looks backwards? + pos = tf.reshape(pos, (1,len(cams),1,1,3)) #NVHWC + return pos + + def get_target_camera_MS_scale_shapes(self): + if self.__target_cameras_MS is not None: + return {scale: cams[0].transform.grid_size for scale, cams in self.__target_cameras_MS.items()} + else: + raise AttributeError("State: base_target_cameras_MS not set.") + + def __make_hull(self, image, eps=1e-5): + return tf.cast(tf.greater_equal(image, eps), dtype=image.dtype) + + @property + def base_targets_raw(self): + if self.__targets_raw is not None: + return self.__targets_raw + else: + raise AttributeError("State: targets_raw not set.") + @base_targets_raw.setter + def base_targets_raw(self, value): + assert is_None_or_type(value, ImageSet) + self.__targets_raw = value + @property + def targets_raw(self): + return self.base_targets_raw.get_images_of_views(self.target_mask) + def targets_raw_MS(self, scale): + return self.base_targets_raw.get_base_images_of_views_MS(scale, self.target_mask) if isinstance(self.base_targets_raw, ImageSetMS) else self.base_targets_raw.get_images_of_views_MS(scale, self.target_mask) + @property + def target_raw_hulls(self): + raise NotImplemented("Deprecated, use state.masks") + return self.__make_hull(self.targets_raw) + def target_raw_hulls_MS(self, scale): + raise NotImplemented("Deprecated, use state.masks_MS") + return self.__make_hull(self.targets_raw_MS(scale)) + + @property + def base_targets(self): + if self.__targets is not None: + return self.__targets + else: + raise AttributeError("State: targets not set.") + @base_targets.setter + def base_targets(self, value): + assert is_None_or_type(value, ImageSet) + self.__targets = value + @property + def targets(self): + return self.base_targets.get_images_of_views(self.target_mask) + def targets_MS(self, scale): + return self.base_targets.get_base_images_of_views_MS(scale, self.target_mask) if isinstance(self.base_targets, ImageSetMS) else self.base_targets.get_images_of_views_MS(scale, self.target_mask) + @property + def target_hulls(self): + raise NotImplemented("Deprecated, use state.masks") + return self.__make_hull(self.targets) + def target_hulls_MS(self, scale): + raise NotImplemented("Deprecated, use state.masks_MS") + return self.__make_hull(self.targets_MS(scale)) + + @property + def base_bkgs(self): + if self.__bkgs is not None: + return self.__bkgs + else: + raise AttributeError("State: backgrounds not set.") + @base_bkgs.setter + def base_bkgs(self, value): + assert is_None_or_type(value, ImageSet) + self.__bkgs = value + @property + def bkgs(self): + return self.base_bkgs.get_images_of_views(self.target_mask) + def bkgs_MS(self, scale): + return self.base_bkgs.get_base_images_of_views_MS(scale, self.target_mask) if isinstance(self.base_bkgs, ImageSetMS) else self.base_bkgs.get_images_of_views_MS(scale, self.target_mask) + + @property + def has_masks(self): + return self.__masks is not None + @property + def base_masks(self): + if self.__masks is not None: + return self.__masks + else: + raise AttributeError("State: image masks not set.") + @base_masks.setter + def base_masks(self, value): + assert is_None_or_type(value, ImageSet) + self.__masks = value + @property + def masks(self): + return self.base_masks.get_images_of_views(self.target_mask) + def masks_MS(self, scale): + return self.base_masks.get_base_images_of_views_MS(scale, self.target_mask) if isinstance(self.base_masks, ImageSetMS) else self.base_masks.get_images_of_views_MS(scale, self.target_mask) + + @classmethod + def from_file(cls, path, frame, transform=None, as_var=True, boundary=None, scale_renderer=None, warp_renderer=None, device=None, density_filename="density.npz", velocity_filename="velocity.npz"): + density = DensityGrid.from_file(os.path.join(path, density_filename), as_var=as_var, scale_renderer=scale_renderer, device=device) + #density = np.load(os.path.join(path, 'density.npz'), allow_pickle=True)['arr_0'].item(0).numpy() #for accidentally pickled tf.Variable + velocity = VelocityGrid.from_file(os.path.join(path, velocity_filename), as_var=as_var, \ + boundary=boundary, scale_renderer=scale_renderer, warp_renderer=warp_renderer, device=device) + state = cls(density, velocity, frame, transform=transform) + return state + + @classmethod + def from_scalarFlow_file(cls, density_path, velocity_path, frame, transform=None, as_var=True, boundary=None, scale_renderer=None, warp_renderer=None, device=None): + density = DensityGrid.from_scalarFlow_file(density_path, as_var=as_var, scale_renderer=scale_renderer, device=device) + #density = np.load(os.path.join(path, 'density.npz'), allow_pickle=True)['arr_0'].item(0).numpy() #for accidentally pickled tf.Variable + velocity = VelocityGrid.from_scalarFlow_file(velocity_path, as_var=as_var, \ + boundary=boundary, scale_renderer=scale_renderer, warp_renderer=warp_renderer, device=device) + state = cls(density, velocity, frame, transform=transform) + return state + + def copy(self, as_var=None, device=None): + s = State(self.density.copy(as_var=as_var, device=device), self.velocity.copy(as_var=as_var, device=device), frame=self.frame, \ + transform=self.transform, targets_raw=self.base_targets_raw, targets=self.base_targets, bkgs=self.base_bkgs) + s.base_target_cameras = self.base_target_cameras + s.target_mask = self.target_mask + # m = copy.copy(self.__dict__) + # del m["_velocity"] + # del m["_density"] + # del m["prev"] + # del m["next"] + # for k,v in m.items(): + # setattr(s,k,v) + return s + + def copy_warped(self, order=1, dt=1.0, frame=None, as_var=None, targets=None, targets_raw=None, bkgs=None, device=None, clamp="NONE"): + d = self.density.copy_warped(order=order, dt=dt, as_var=as_var, device=device, clamp=clamp) + v = self.velocity.copy_warped(order=order, dt=dt, as_var=as_var, device=device, clamp=clamp) + return State(d, v, frame, transform=self.transform, targets=targets, targets_raw=targets_raw, bkgs=bkgs) + + def get_density_transform(self): + if isinstance(self.transform, GridTransform): + # density_transform = copy.copy(self.transform) + # density_transform.set_data(self.density.d) + density_transform = self.transform.copy_new_data(self.density.d) + return density_transform + else: + raise TypeError("state.transform is not a GridTransform") + + def get_density_transform_MS(self, scale): + if isinstance(self.transform, GridTransform): + # density_transform = copy.copy(self.transform) + # density_transform.set_data(self.density.d) + density_transform = self.transform.copy_new_data(self.density.d_MS(scale)) + return density_transform + else: + raise TypeError("state.transform is not a GridTransform") + + def get_velocity_transform(self): + if isinstance(self.transform, GridTransform): + # velocity_transform = copy.copy(self.transform) + # velocity_transform.set_data(self.velocity.lod_pad) + return self.transform.copy_new_data(self.velocity.lod_pad) + else: + raise TypeError("state.transform is not a GridTransform") + + def render_density(self, render_ctx, custom_ops=None, keep_SDF_positions=False, super_sampling=1): + # imgs = tf.concat(render_ctx.dens_renderer.render_density(self.get_density_transform(), render_ctx.lights, render_ctx.cameras, cut_alpha=False, monochrome=render_ctx.monochrome), axis=0) #, background=bkg + if render_ctx.render_SDF: + if keep_SDF_positions: + imgs, pos = render_ctx.dens_renderer.render_SDF(self.get_density_transform(), light_list=render_ctx.lights, camera_list=self.target_cameras, cut_alpha=False, monochrome=render_ctx.monochrome, custom_ops=custom_ops, output_positions=True, super_sampling=super_sampling) + imgs = tf.stack(imgs, axis=1) + pos = tf.stack(pos, axis=1) + self.SDF_positions = pos + else: + imgs = tf.stack(render_ctx.dens_renderer.render_SDF(self.get_density_transform(), light_list=render_ctx.lights, camera_list=self.target_cameras, cut_alpha=False, monochrome=render_ctx.monochrome, custom_ops=custom_ops, super_sampling=super_sampling), axis=1) + imgs, t = tf.split(imgs, [3,1], axis=-1) + else: + if not super_sampling==1: raise NotImplementedError + imgs = tf.stack(render_ctx.dens_renderer.render_density(self.get_density_transform(), light_list=render_ctx.lights, camera_list=self.target_cameras, cut_alpha=False, monochrome=render_ctx.monochrome, custom_ops=custom_ops), axis=1) #, background=bkg + imgs, d = tf.split(imgs, [3,1], axis=-1) + t = tf.exp(-d) + self.images = imgs + self.t = t + + def render_density_MS_stack(self, render_ctx, scale_shapes=None, custom_ops=None, keep_SDF_positions=False, super_sampling=1): + if not self.density.is_MS: + raise RuntimeError("Frame {} has no MS density to render.".format(self.frame)) + if scale_shapes is None: + scale_shapes = {scale: shape[1:] for scale, shape in self.get_target_camera_MS_scale_shapes().items()} + self._density_images_MS = {} + self._density_images_t_MS = {} + self._SDF_positions_MS = {} + for scale, shape in scale_shapes.items(): + if render_ctx.render_SDF: + if keep_SDF_positions: + imgs, pos = render_ctx.dens_renderer.render_SDF(self.get_density_transform_MS(scale), light_list=render_ctx.lights, camera_list=self.target_cameras_MS(scale), cut_alpha=False, monochrome=render_ctx.monochrome, custom_ops=custom_ops, output_positions=True, super_sampling=super_sampling) + imgs = tf.stack(imgs, axis=1) + pos = tf.stack(pos, axis=1) + self._SDF_positions_MS[scale] = pos + else: + imgs = tf.stack(render_ctx.dens_renderer.render_SDF(self.get_density_transform_MS(scale), light_list=render_ctx.lights, camera_list=self.target_cameras_MS(scale), cut_alpha=False, monochrome=render_ctx.monochrome, custom_ops=custom_ops, super_sampling=super_sampling), axis=1) #, background=bkg + imgs, t = tf.split(imgs, [3,1], axis=-1) + else: + if not super_sampling==1: raise NotImplementedError + imgs = tf.stack(render_ctx.dens_renderer.render_density(self.get_density_transform_MS(scale), light_list=render_ctx.lights, camera_list=self.target_cameras_MS(scale), cut_alpha=False, monochrome=render_ctx.monochrome, custom_ops=custom_ops), axis=1) #, background=bkg + imgs, d = tf.split(imgs, [3,1], axis=-1) + t = tf.exp(-d) + self._density_images_MS[scale] = imgs + self._density_images_t_MS[scale] = t + + + def images_MS(self, scale): + return self._density_images_MS[scale] + def t_MS(self, scale): + return self._density_images_t_MS[scale] + def SDF_positions_MS(self, scale): + return self._SDF_positions_MS[scale] + @property + def image_masks(self): + if not self.has_density or not self.density.is_SDF: + raise RuntimeError() + if self.t is None: + raise RuntimeError("images have not been rendered.") + return 1.0 - self.t + def image_masks_MS(self, scale): + if not self.has_density or not self.density.is_SDF: + raise RuntimeError() + if scale not in self._density_images_t_MS: + raise RuntimeError("Images for scale %s have not been rendered."%(scale,)) + return 1.0 - self._density_images_t_MS[scale] + + def density_advected(self, dt=1.0, order=1, clamp="NONE"): + return self.density.warped(self.velocity, order=order, dt=dt, clamp=clamp)#self.velocity.warp(self.density, scale_renderer) + def velocity_advected(self, dt=1.0, order=1, clamp="NONE"): + return self.velocity.copy_warped(order=order, dt=dt, as_var=False, clamp=clamp) + + def rescale_density(self, shape, device=None): + #density = renderer.resample_grid3D_aligned(self.density, dens_shape) + self.density = self.density.copy_scaled(shape, device=device) + def rescale_velocity(self, shape, scale_magnitude=True, device=None): + self.velocity = self.velocity.copy_scaled(shape, scale_magnitude=scale_magnitude, device=device) + def rescale(self, dens_shape, vel_shape, device=None): + rescale_density(self, dens_shape, device=device) + rescale_velocity(self, vel_shape, device=device) + + def var_list(self): + var_list = [] + if self.has_density: + var_list += self.density.var_list() + if self.has_velocity: + var_list += self.velocity.var_list() + return var_list + + def get_variables(self): + var_dict = {} + if self.has_density: + var_dict.update(self.density.get_variables()) + if self.has_velocity: + var_dict.update(self.velocity.get_variables()) + return var_dict + + def get_output_variables(self, centered=True, staggered=True, include_MS=False, include_residual=False): + var_dict = {} + if self.has_density: + var_dict.update(self.density.get_output_variables(include_MS=include_MS, include_residual=include_residual)) + if self.has_velocity: + var_dict.update(self.velocity.get_output_variables(centered=centered, staggered=staggered, include_MS=include_MS, include_residual=include_residual)) + return var_dict + + + def stats(self, vel_scale=[1,1,1], mask=None, render_ctx=None, **warp_kwargs): + target_stats = None + if render_ctx is not None and getattr(self, "target_cameras", None) is not None: + target_stats = {} + self.render_density(render_ctx) + if getattr(self, "targets_raw") is not None and getattr(self, "bkgs") is not None: + target_stats["SE_raw"] = tf_tensor_stats(tf.math.squared_difference(self.images + self.bkgs*self.t, self.targets_raw), as_dict=True) + if getattr(self, "targets") is not None: + target_stats["SE"] = tf_tensor_stats(tf.math.squared_difference(self.images, self.targets), as_dict=True) + return self.density.stats(mask=mask, state=self, **warp_kwargs), self.velocity.stats(vel_scale, mask=mask, state=self, **warp_kwargs), target_stats + + def stats_target(self, vel_scale=[1,1,1], mask=None, **warp_kwargs): + return self.density_target.stats(mask=mask, state=self, **warp_kwargs) if self.has_density_target else None, \ + self.velocity_target.stats(vel_scale, mask=mask, state=self, **warp_kwargs) if self.has_velocity_target else None + + def save(self, path, suffix=None): + self.density.save(os.path.join(path, 'density.npz' if suffix is None else 'density_'+suffix+'.npz')) + self.velocity.save(os.path.join(path, 'velocity.npz' if suffix is None else 'velocity_'+suffix+'.npz')) + if self.has_density_proxy: + self.density_proxy.save(os.path.join(path, 'density_proxy.npz' if suffix is None else 'density_proxy_'+suffix+'.npz')) + if self.has_density_target: + self.density_target.save(os.path.join(path, 'density_target.npz' if suffix is None else 'density_target_'+suffix+'.npz')) + + + def clear_cache(self): + self.images = None + self.t = None + self.SDF_positions = None + self._density_images_MS = {} + self._density_images_t_MS = {} + self._SDF_positions_MS = {} + if self.has_density: self.density.clear_cache() + if self.has_density_proxy: self.density_proxy.clear_cache() + if self.has_velocity: self.velocity.clear_cache() + + @property + def is_MS(self): + return (((not self.has_density) or self.density.is_MS) \ + and ((not self.has_velocity) or self.velocity.is_MS)) + +#import phitest.render.neural_data_structures as NDS + +class Sequence: + def __init__(self, states): + #raise NotImplementedError("TODO") + #assert len(states)==len(ctxs) + self.sequence = [state for state in states] + #self.contexts = [ctx for ctx in ctxs] + + class SequenceIterator: + def __init__(self, sequence): + self.seq = sequence + self.idx = 0 + def __next__(self): + if self.idx0: + s[i].prev = s[i-1] + if i<(len(s)-1): + s[i].next = s[i+1] + return Sequence(s) + + # Defined in neural_data_structures.py to avoid circular dependencies. + # def set_density_for_neural_globt(self, as_var=False, device=None, dt=1.0, order=1, clamp='NONE'): + # for i, state in enumerate(self): + # if i>0: + # #state.density = state.density.copy(as_var=as_var, device=device) + # #state.density = state.density.copy_empty(as_var=as_var, device=device) + # state.density = NDS.WarpedDensityGrid(order=order, dt=dt, clamp=clamp, device=device, scale_renderer=state.density.scale_renderer, is_SDF=state.density.is_SDF) + # state.density.parent_state = state + + + def copy_for_neural_globt(self, as_var=False, device=None): + #assert all(isinstance(state, NeuralState) for state in self) + s = [copy.copy(_) for _ in self] + for i, state in enumerate(s): + state.velocity = copy.copy(state.velocity) + state.velocity.parent_state = state + + if i==0: + state.density = copy.copy(state.density) + else: + state.density = state.density.copy(as_var=as_var, device=device) + state.density.parent_state = state + + if i>0: + state.prev = s[i-1] + if i<(len(s)-1): + state.next = s[i+1] + s = Sequence(s) + s.clear_cache() + return s + + def copy_from_targets(self, as_var=False, device=None): + #assert all(isinstance(state, NeuralState) for state in self) + s = [copy.copy(_) for _ in self] + for i, state in enumerate(s): + if state.has_velocity_target: + state.velocity = state.velocity_target.copy(as_var=as_var, device=device) + state.velocity.parent_state = state + else: + state.velocity = None + + if state.has_density_target: + state.density = state.density_target.copy(as_var=as_var, device=device) + state.density.parent_state = state + else: + state.density = None + + + if i>0: + state.prev = s[i-1] + if i<(len(s)-1): + state.next = s[i+1] + s = Sequence(s) + s.clear_cache() + return s + + def copy_from_proxy(self, as_var=False, device=None): + #assert all(isinstance(state, NeuralState) for state in self) + s = [copy.copy(_) for _ in self] + for i, state in enumerate(s): + if state.has_velocity: + state.velocity = state.velocity.copy(as_var=as_var, device=device) + state.velocity.parent_state = state + else: + state.velocity = None + + if state.has_density_proxy: + state.density = state.density_proxy #.copy(as_var=as_var, device=device) + state.density.parent_state = state + else: + state.density = None + + + if i>0: + state.prev = s[i-1] + if i<(len(s)-1): + state.next = s[i+1] + s = Sequence(s) + s.clear_cache() + return s + + def get_sub_sequence(self, length): + if length>len(self): + raise ValueError("Sequence has a length of %d"%(len(self),)) + + s = [self[i] for i in range(length)] + s = Sequence(s) + s.restore_connections() + + return s + + def restore_connections(self): + for i, state in enumerate(self): + if state.has_velocity: + state.velocity.parent_state = state + + if state.has_density: + state.density.parent_state = state + + if state.has_density_proxy: + state.density_proxy.parent_state = state + + if i>0: + state.prev = self[i-1] + else: + state.prev = None + + if i<(len(self)-1): + state.next = self[i+1] + else: + state.next = None + + def insert_state(self, state, idx): + raise NotImplementedError("need to set state.next and state.prev") + self.sequence.insert(state, idx) + #self.contexts.insert(ctx, idx) + + def append_state(self, state): + raise NotImplementedError("need to set state.next and state.prev") + self.sequence.append(state) + #self.contexts.append(ctx) + + def start_iteration(self, iteration): + for state in self: + ctx.start_iteration(iteration) + + def stats(self, vel_scale=[1,1,1], mask=None, **warp_kwargs): + return [_.stats(vel_scale, mask=mask, state=_, **warp_kwargs) for _ in self] + + def save(self, path=None, suffix=None): + for state in self: + if path is None and hasattr(state, 'data_path'): + state.save(state.data_path, suffix) + else: + state.save(os.path.join(path, 'frame_{:06d}'.format(state.frame)), suffix) + + def clear_cache(self): + for state in self: state.clear_cache() + @property + def advect_steps(self): + return len(self)-1 + + def copy_densities_advect_fwd(self, dt=1.0, order=1, clamp='NONE'): + # + raise NotImplementedError + LOG.warning("Sequence.copy_densities_advect_fwd() only creates new density objects.") + s = [] + for i in range(1, len(self)): + pass + + def densities_advect_fwd(self, dt=1.0, order=1, clamp='NONE', print_progress=None, clear_cache=False): + raise NotImplementedError("Deprecated, use set_density_for_neural_globt()") + if clear_cache: + LOG.warning("densities_advect_fwd with cleared cache.") + #raise NotImplementedError + if clamp is None or clamp.upper() not in ['LOCAL', 'GLOBAL']: + for i in range(1, len(self)): + self[i].density.assign(self[i-1].density_advected(order=order, dt=dt, clamp=clamp)) + #copy_warped(self, vel_grid, as_var=None, order=1, dt=1.0, device=None, var_name=None, clamp="NONE", trainable=None, restrict_to_hull=None) + # self[i].density = self[i-1].density.copy_warped(self[i-1].velocity, as_var=False, order=order, dt=dt, clamp=clamp, device=self[i-1].density._device, trainable=False, var_name="DensFwdAdv") + # self[i].density.parent_state = self[i] + if print_progress is not None: print_progress.update(desc="Advect density {: 3d}/{: 3d}".format(i, len(self)-1)) + if clear_cache and i>1: + self[i-2].clear_cache() + elif clamp.upper()=='LOCAL': #clamp after each step, before the next warp + for i in range(1, len(self)): + self[i].density.assign(tf.maximum(self[i-1].density_advected(order=order, dt=dt), 0)) + if print_progress is not None: print_progress.update(desc="Advect density {: 3d}/{: 3d}".format(i, len(self)-1)) + elif clamp.upper()=='GLOBAL': #clamp after all warping + for i in range(1, len(self)): + self[i].density.assign(self[i-1].density_advected(order=order, dt=dt)) + if print_progress is not None: print_progress.update(desc="Advect density {: 3d}/{: 3d}".format(i, len(self)-1)) + for i in range(1, len(self)): + self[i].density.assign(tf.maximum(self[i].density._d, 0)) + def velocities_advect_fwd(self, dt=1.0, order=1, clamp='NONE'): + #raise NotImplementedError + for i in range(1, len(self)): + self[i].velocity.assign(*self[i-1].velocity.warped(order=order, dt=dt, clamp=clamp)) + + def generate_outputs(self, clear_cache=False): + if clear_cache: + self.clear_cache() + for state in self: + state.density.d + state.velocity.centered() \ No newline at end of file diff --git a/phitest/render/discriminator.py b/phitest/render/discriminator.py new file mode 100644 index 0000000..99a8091 --- /dev/null +++ b/phitest/render/discriminator.py @@ -0,0 +1,2 @@ + + diff --git a/phitest/render/generator_models.py b/phitest/render/generator_models.py new file mode 100644 index 0000000..975740b --- /dev/null +++ b/phitest/render/generator_models.py @@ -0,0 +1,2373 @@ +import tensorflow as tf +import numbers, json, copy, math +from collections.abc import Iterable +from lib.tf_ops import * +import munch +import logging +from .vector import GridShape +from .profiling import SAMPLE +LOG = logging.getLogger('Generators') +LOG.setLevel(logging.DEBUG) + +""" + Density: +target view encoder: 2D target -> 3D latent/features, for single view, shared in multi view +view combination: 3D latent/features of multiple views together, may involve resampling/transformation based on view calibration +volume decoder: combined 3D latent/features to 3D density volume + + Velcoity: + +""" +class GeneratorSingleviewEncoder2D: + """ + simple conv stack to encode a single target + """ + def __init__(self, targets): + raise NotImplementedError() + +class GeneratorSingleviewLifting: + """ + transform a 2D (latent) tensor to 3D + """ + def __init__(self, model_encoder): + raise NotImplementedError() + +class GeneratorMultiviewMerge3D: + """ + merge several (latent) 3D tensors, respecting camera calibration of the corresponding, encoded targets (if available) + simple number-invariant combination of tensors(e.g. addition) followed by some convolutions + """ + def __init__(self, embeddings_v): + raise NotImplementedError() + +class GeneratorDensityDecoder3D: + """ + decode a (latent) 3D tensor into a volume of density + may get additional embeddings of past and future states/targets; or decoded past densities + """ + def __init__(self, embeddings_t): + raise NotImplementedError() + + + +class GeneratorVelocity: + """ + decode a (latent) 3D tensor into a volume of velocities + may get additional embeddings of past and future states/targets + last/past (adevcted) velocity for coherence + """ + def __init__(self, density_or_embedding, veloicty_prev): + raise NotImplementedError() + + +def get_view_encoder(input_channels=3, layers=[4]*6, kernel_size=3, strides=1, skip_indices=None, skips={}, activation='relu', alpha=0.2, noise_std=0.0, padding='ZERO', skip_mode="CONCAT"): + input_shape = [None,None,input_channels] + dim = len(input_shape)-1 + num_layers = len(layers) + skip_mode = skip_mode.upper() + if np.isscalar(strides): + strides = [strides]*num_layers + if np.isscalar(kernel_size): + kernel_size = [kernel_size]*num_layers + if skip_indices is None: + skip_indices = [0]*num_layers + x = tf.keras.layers.Input(shape=input_shape, name='view-enc_input') + inputs = x + if noise_std>0: + x = tf.keras.layers.GaussianNoise(stddev=noise_std)(x) + for filters, stride, ks, skip_idx in zip(layers, strides, kernel_size, skip_indices): + if skip_idx<0: + idx = abs(skip_idx) + if idx not in skips: raise ValueError("Skip connection %d not defined."%idx) + if skip_mode=="CONCAT": + x = tf.keras.layers.Concatenate(axis=-1)([x, skips[idx]]) + elif skip_mode=="ADD": + x = tf.keras.layers.Add()([x, skips[idx]]) + else: raise ValueError("Unknown skip mode '%s'"%(skip_mode,)) + x = ConvLayer(x, dim, filters, ks, stride, activation, alpha, padding=padding) + if skip_idx>0: + if skip_idx in skips: raise ValueError("Skip connection %d already defined."%skip_idx) + skips[skip_idx] = x + outputs = x + return tf.keras.Model(inputs=[inputs], outputs=[outputs]) + + +def get_density_decoder(input_channels=4, layers=[4]*6, kernel_size=3, strides=1, skip_indices=None, skips={}, activation='relu', alpha=0.2, noise_std=0.0, padding='ZERO', skip_mode="CONCAT"): + input_shape = [None,None,None,input_channels] + dim = len(input_shape)-1 + num_layers = len(layers) + skip_mode = skip_mode.upper() + if np.isscalar(strides): + strides = [strides]*num_layers + if np.isscalar(kernel_size): + kernel_size = [kernel_size]*num_layers + if skip_indices is None: + skip_indices = [0]*num_layers + x = tf.keras.layers.Input(shape=input_shape, name='dens-gen_input') + inputs = x + if noise_std>0: + x = tf.keras.layers.GaussianNoise(stddev=noise_std)(x) + for filters, stride, ks, skip_idx in zip(layers, strides, kernel_size, skip_indices): + if skip_idx<0: + idx = abs(skip_idx) + if idx not in skips: raise ValueError("Skip connection %d not defined."%idx) + if skip_mode=="CONCAT": + x = tf.keras.layers.Concatenate(axis=-1)([x, skips[idx]]) + elif skip_mode=="ADD": + x = tf.keras.layers.Add()([x, skips[idx]]) + else: raise ValueError("Unknown skip mode '%s'"%(skip_mode,)) + x = ConvLayer(x, dim, filters, ks, stride, activation, alpha, padding=padding) + if skip_idx>0: + if skip_idx in skips: raise ValueError("Skip connection %d already defined."%skip_idx) + skips[skip_idx] = x + x = ConvLayer(x, dim, 1, 3, activation="relu", padding=padding, name='dens-gen_output') + outputs = x + return tf.keras.Model(inputs=[inputs], outputs=[outputs]) + +def get_velocity_decoder(input_channels=4, layers=[4]*6, kernel_size=3, strides=1, skip_indices=None, skips={}, activation='relu', alpha=0.2, noise_std=0.0, padding='ZERO'): + input_shape = [None,None,None,input_channels] + dim = len(input_shape)-1 + num_layers = len(layers) + if np.isscalar(strides): + strides = [strides]*num_layers + if skip_indices is None: + skip_indices = [0]*num_layers + x = tf.keras.layers.Input(shape=input_shape, name='vel-gen_input') + inputs = x + if noise_std>0: + x = tf.keras.layers.GaussianNoise(stddev=noise_std)(x) + for filters, stride, skip_idx in zip(layers, strides, skip_indices): + if skip_idx<0: + idx = abs(skip_idx) + if idx not in skips: raise ValueError("Skip connection %d not defined."%idx) + x = tf.keras.layers.Concatenate(axis=-1)([x, skips[idx]]) + x = ConvLayer(x, dim, filters, kernel_size, stride, activation, alpha, padding=padding) + if skip_idx>0: + if skip_idx in skips: raise ValueError("Skip connection %d already defined."%skip_idx) + skips[skip_idx] = x + x = ConvLayer(x, dim, 3, kernel_size, padding=padding, name='vel-gen_output') + outputs = x + return tf.keras.Model(inputs=[inputs], outputs=[outputs]) + +class ChannelPadding(tf.keras.layers.Layer): + def __init__(self, padding, constant=0, **kwargs): + if not isinstance(constant, numbers.Number): raise ValueError("constant must be a scalar.") + if isinstance(padding, numbers.Integral): + padding = (padding, padding) + elif (not isinstance(padding, (list, tuple))) or (not len(padding)==2) or (not isinstance(padding[0], numbers.Integral)) or (not isinstance(padding[1], numbers.Integral)): + raise ValueError("padding must be int or tuple of 2 int.") + super().__init__(**kwargs) + LOG.debug("Create ChannelPadding '%s': pad=%s, c=%s", self.name, padding, constant) + self.padding = padding + self.constant = constant + + def _get_paddings(self, tensor_rank): + return [(0,0)]*(tensor_rank-1) + [self.padding] + + def call(self, inputs): + return tf.pad(inputs, self._get_paddings(tf.rank(inputs).numpy()), mode="CONSTANT", constant_values=self.constant) + + def compute_output_shape(self, input_shapes): + if isinstance(input_shapes, list): + assert len(input_shapes)==1 + input_shapes = input_shapes[0] + output_shape = list(input_shapes)[:-1] + output_shape.append(input_shapes[-1] + self.padding[0] + self.padding[1]) + output_shape = tf.TensorShape(output_shape) + return output_shape + + def get_config(self): + config = super().get_config() + config.update({"padding":self.padding, "constant":self.constant}) + return config + +class WeightedSum(tf.keras.layers.Add): + def __init__(self, alpha, **kwargs): + super().__init__(**kwargs) + LOG.debug("Create WeightedSum '%s': alpha=%s", self.name, alpha) + self.alpha = tf.keras.backend.variable(alpha, dtype="float32", name="ws_alpha") + + def _merge_function(self, inputs): + assert (len(inputs)==2), "WeightedSum takes 2 inputs" + LOG.debug("WeightedSum '%s' merging inputs %s and %s", self.name, shape_list(inputs[0]), shape_list(inputs[1])) + return ((1.0-self.alpha) * inputs[0]) + (self.alpha * inputs[1]) + + @property + def variables(self): + return [self.alpha] + @property + def trainable_variables(self): + return [] + @property + def non_trainable_variables(self): + return [self.alpha] + + + @property + def trainable_weights(self): + return [] + @property + def non_trainable_weights(self): + return [] + @property + def weights(self): + return [] + + def get_config(self): + config = super().get_config() + config.update({"alpha":self.alpha.numpy().tolist()}) + return config + +class ScalarMul(tf.keras.layers.Layer): + def __init__(self, alpha, **kwargs): + super().__init__(**kwargs) + LOG.debug("Create ScalarMul '%s': alpha=%s", self.name, alpha) + self.alpha = tf.keras.backend.variable(alpha, dtype="float32", name="smul_alpha") + + def call(self, inputs): + LOG.debug("WeightedSum '%s' scaling %s with %s", self.name, shape_list(inputs), self.alpha) + return inputs * self.alpha + + @property + def variables(self): + return [self.alpha] + @property + def trainable_variables(self): + return [] + @property + def non_trainable_variables(self): + return [self.alpha] + + + @property + def trainable_weights(self): + return [] + @property + def non_trainable_weights(self): + return [] + @property + def weights(self): + return [] + + def get_config(self): + config = super().get_config() + config.update({"alpha":self.alpha.numpy().tolist()}) + return config + +class UnprojectionLayer(tf.keras.layers.Layer): + def __init__(self, grid_transform, cameras, renderer, merge_mode="MEANPROD", **layer_kwargs): + super().__init__(**layer_kwargs) + self.__renderer = renderer + self.__cameras = copy.copy(cameras) + self._set_output_transform(grid_transform) + assert merge_mode in ["SUM", "MEAN", "PROD", "SUMPROD", "MEANPROD"] + self.__merge_mode = merge_mode + if not self.__renderer.blend_mode=="ADDITIVE": + raise ValueError("target merging requires ADDITIVE blend mode used in target lifting") + #assert isinstance(spatial_output_shape, (list, tuple)) and len(spatial_output_shape)==3 and all(isinstance(dim, numbers.Integral) for dim in spatial_output_shape) + #self.__shape = copy.copy(spatial_output_shape) + + def get_num_views(self): + return len(self.__cameras) + + def set_output_shape(self, shape): + assert isinstance(shape, (list, tuple)) and len(shape)==3 and all(isinstance(dim, numbers.Integral) for dim in shape) + self.__transform.grid_size = shape + + def _set_output_transform(self, grid_transform): + assert grid_transform is not None + self.__transform = grid_transform.copy_no_data() + + def __unproject_cameras(self, tensor, cameras): + shape = GridShape.from_tensor(tensor) + with SAMPLE("unproject %s-%d>%s"%([shape.y, shape.x],cameras[0].transform.grid_size[0], self.__transform.grid_size)): + # image shape for unprojection: NVHWC with C in [1,2,4] + if shape.c not in [1,2,4]: + channel_div = shape.c//4 if shape.c%4==0 else shape.c//2 if shape.c%2==0 else shape.c + tensor = tf.reshape( \ + tf.transpose( \ + tf.reshape(tensor, (shape.n, shape.z, shape.y, shape.x, channel_div, shape.c//channel_div)), \ + (0,4,1,2,3,5)), \ + (shape.n * channel_div, shape.z, shape.y, shape.x, shape.c//channel_div) \ + ) + #raise NotImplementedError("can only unproject with channels 1,2 or 4. is %d"%shape.c) + # roll channels into batch? + # camera XY resolution does not matter, raymarch_camera uses the input image resolution + + tensor = self.__renderer.raymarch_camera(data=tensor, cameras=cameras, transformations=self.__transform, inverse=True, squeeze_batch=False) + vol_shape = GridShape.from_tensor(tensor) + if shape.c not in [1,2,4]: + tensor = tf.reshape( \ + tf.transpose( \ + tf.reshape(tensor, (shape.n, channel_div, vol_shape.z, vol_shape.y, vol_shape.x, shape.c//channel_div)), \ + (0,2,3,4,1,5)), \ + (shape.n, vol_shape.z, vol_shape.y, vol_shape.x, shape.c) \ + ) + return tensor + + def _unproject(self, tensor): + assert isinstance(tensor, tf.Tensor) + shape = shape_list(tensor) + assert len(shape)==5 #NVHWC + shape = GridShape.from_tensor(tensor) + num_views = self.get_num_views() + assert num_views==shape.z, "view dimension does not match camera list" + + if self.__merge_mode not in ["SUM", "MEAN"]: + tensors = tf.split(tensor, shape.z, axis=1) #V-N1WHC + shape.z = 1 + cameras = [[cam] for cam in self.__cameras] + else: + #if not self.lifting_renderer.blend_mode=="ADDITIVE": + # raise ValueError("SUM and MEAN unprojection merging requires ADDITIVE blend mode used in target lifting") + tensors = [tensor] #1-NVWHC + cameras = [self.__cameras] + + data = [] + for tensor, cams in zip(tensors, cameras): + data.append(self.__unproject_cameras(tensor, cams)) + + with SAMPLE("merge"): + if num_views==1 or self.__merge_mode=="SUM": + return data[0] + elif self.__merge_mode=="MEAN": + return data[0] * tf.constant(1.0/num_views, dtype=data[0].dtype) + elif self.__merge_mode=="PROD": + return tf.reduce_prod([tf.tanh(_) for _ in data], axis=0) + elif self.__merge_mode in ["SUMPROD", "MEANPROD"]: + add_channels = shape.c//2 + + add_data = tf.reduce_sum([_[...,:add_channels] for _ in data], axis=0) + mul_data = tf.reduce_prod([tf.tanh(_[...,add_channels:]) for _ in data], axis=0) + + if self.__merge_mode=="MEANPROD": + add_data = add_data * tf.constant(1.0/num_views, dtype=data[0].dtype) + + return tf.concat([add_data, mul_data], axis=-1) + + + + def call(self, inputs): + with SAMPLE("UnprojectionLayer"): + inp_shape = shape_list(inputs) + assert len(inp_shape)==4 #NHWC + inputs = tf.expand_dims(inputs, axis=1) #->N1HWC + shape = GridShape.from_tensor(inputs) + + tensor = self._unproject(inputs) + + return tensor + + def compute_output_shape(self, input_shapes): + if isinstance(input_shapes, list): + assert len(input_shapes)==1 + input_shapes = input_shapes[0] + output_shape = [input_shapes[0]] + self.__transform.grid_size + [input_shapes[-1]] #NDHWC + output_shape = tf.TensorShape(output_shape) + return output_shape + + @property + def variables(self): + return [] + @property + def trainable_variables(self): + return [] + @property + def non_trainable_variables(self): + return [] + + + @property + def trainable_weights(self): + return [] + @property + def non_trainable_weights(self): + return [] + @property + def weights(self): + return [] + + def get_config(self): + config = super().get_config() + #config.update(self.cnf) + return config + + @classmethod + def from_config(cls, config_dict): + raise NotImplementedError("serialization of camera and transform") + return cls(**config_dict) + +class GridSamplingLayer(tf.keras.layers.Layer): + def __init__(self, spatial_output_shape, filter_mode="LINEAR", mipmapping="NONE", mip_levels="AUTO", boundary_mode="CLAMP", sample_gradients=False, **kwargs): + super().__init__(**kwargs) + self._renderer = Renderer(filter_mode=filter_mode, boundary_mode=boundary_mode, \ + mipmapping=mipmapping, num_mips=0, mip_bias=0, sample_gradients=sample_gradients, \ + name=self.name+"_GSLrenderer") + self.__filter_mode=filter_mode + self.__mipmapping=mipmapping + self.__mip_levels=mip_levels + self.__boundary_mode=boundary_mode + self.__sample_gradients=sample_gradients + assert has_shape(spatial_output_shape, [3]) + self._output_shape = spatial_output_shape + + def call(self, inputs): + if self.mip_levels=="AUTO": + pass + LOG.debug("GridSamplingLayer '%s' sampling %s to %s", self.name, shape_list(inputs), self.alpha) + return self._renderer.resample_grid3D_aligned(inputs, self._output_shape) + + def compute_output_shape(self, input_shapes): + if isinstance(input_shapes, list): + assert len(input_shapes)==1 + input_shapes = input_shapes[0] + output_shape = list(self._output_shape) + output_shape.append(input_shapes[-1]) + output_shape = tf.TensorShape(output_shape) + return output_shape + + @property + def variables(self): + return [] + @property + def trainable_variables(self): + return [] + @property + def non_trainable_variables(self): + return [] + + + @property + def trainable_weights(self): + return [] + @property + def non_trainable_weights(self): + return [] + @property + def weights(self): + return [] + + def get_config(self): + config = super().get_config() + config.update({ + "spatial_output_shape":self._output_shape, + "filter_mode":self.__filter_mode, + "mipmapping":self.__mipmapping, + "mip_levels":self.__mip_levels, + "boundary_mode":self.__boundary_mode, + "sample_gradients":self.__sample_gradients, + }) + return config + +# get object from dict if it exist, otherwise construct, add and return it +def dict_get_make(d, k, v_fn=lambda: None): + if k not in d: + d[k] = v_fn() + return d[k] + +def normalize_block_config(block_config, num_levels, is_shared): + # list of str if shared + # list of list of str + if not isinstance(block_config, (list, tuple)): + raise TypeError("Invalid Block configuration. must be list(list(str)) or list(str), is: %s"%(block_config,)) + is_single = len(block_config)==0 or isinstance(block_config[0], str) + if is_shared and (not is_single) and len(block_config)!=1: + raise ValueError("Block configuration must be a list(str) or [list(str)] for shared layers, is: %s"%(block_config,)) + if (not is_shared) and (not is_single) and (len(block_config)!=num_levels): + raise ValueError("Block configuration must be specified for %d levels, is: %s"%(num_levels, block_config,)) + for sub in block_config: + if (isinstance(sub, str) and not is_single) or (isinstance(sub, (list, tuple)) and is_single): + raise TypeError("Invalid Block configuration. must be list(list(str)) or list(str), is: %s"%(block_config,)) + if not is_single: + for block in sub: + if not isinstance(block, str): + raise TypeError("Invalid Block configuration. must be list(list(str)) or list(str), is: %s"%(block_config,)) + if is_single: + block_config = [block_config] * (1 if is_shared else num_levels) + return block_config + +# from optimizer.py +def _var_key(var): + # if hasattr(var, "op"): + # return (var.op.graph, var.op.name) + return var._unique_id + +class DeferredBackpropNetwork: + def __init__(self, model, name="DeferredBackpropNetwork"): + assert isinstance(model, tf.keras.models.Model) + self._model = model + assert isinstance(name, str) + self.name = name + self._frozen = False + self._clear_gradient_accumulators() + self.clear_gradients() + + def __call__(self, inputs): + return self._model(inputs) + + @property + def trainable_variables(self): + return self._model.trainable_variables + + @property + def is_frozen_weights(self): + return self._frozen + def set_frozen_weights(self, freeze): + if freeze: + self.freeze_weights() + else: + self.unfreeze_weights() + def freeze_weights(self): + if self.has_pending_gradients: + raise RuntimeError("DeferredBackpropNetwork: clear gradients before freezing weights.") + self._frozen = True + def unfreeze_weights(self): + self._frozen = False + + def _get_gradient_accumulator(self, var): + #assert isinstance(var, tf.Variable) + #if not hasattr(self, "_grad_acc"): + # self._grad_acc = {} + var_key = _var_key(var) + if var_key not in self._grad_acc: + self._grad_acc[var_key] = tf.Variable(tf.zeros_like(var), trainable=False) + return self._grad_acc[var_key] + + def _get_gradient_accumulators(self): + variables = self.trainable_variables + # if len(self._grad_acc)>len(variables): #can have more if reducing recursions + # raise RuntimeError("%s built %d gradient accumulators, but has only %d variables."%(self.name, len(self._grad_acc), len(variables))) + # elif len(self._grad_acc)0 + @property + def num_pending_gradients(self): + return self._num_pending_gradients + + def _get_gradient_normalization_factor(self): + return tf.constant((1/self._num_pending_gradients) if self._num_pending_gradients>0 else 1, dtype=tf.float32) + + def get_pending_gradients(self, normalize=False): + if normalize: + s = self._get_gradient_normalization_factor() + return [tf.identity(_)*s for _ in self._get_gradient_accumulators()] + else: + return [tf.identity(_) for _ in self._get_gradient_accumulators()] + + def get_pending_gradients_summary(self, normalize=False): + s = 1.0/self._num_pending_gradients if normalize and self._num_pending_gradients>0 else 1.0 + return [[tf.reduce_mean(_).numpy()*s, tf.reduce_max(tf.abs(_)).numpy()*s] for _ in self._get_gradient_accumulators()] + + def get_weights_summary(self): + return [[tf.reduce_mean(_).numpy(), tf.reduce_max(tf.abs(_)).numpy()] for _ in self.trainable_variables] + + def compute_regularization_gradients(self, weight=1.0, add_gradients=False): + weights = self.trainable_variables + with tf.GradientTape(persistent=True, watch_accessed_variables=False) as tape: + tape.watch(weights) + #loss = tf.reduce_mean([tf.reduce_mean(tf.nn.l2_loss(var)) for var in weights]) * weight + loss = tf.reduce_sum([tf.reduce_sum(tf.nn.l2_loss(var)) for var in weights]) * weight + grads = tape.gradient(loss, weights) + if add_gradients: + self.add_gradients(grads) + return grads + + def summary(self, print_fn=print): + self._model.summary(print_fn=print_fn) + + def save(self, path): + model_path = path + path = path.replace("_model.h5",".json") + self._model.save(model_path) + + cnfg = munch.Munch() + cnfg._config = self.get_config() + + with open(path, "w") as config_file: + json.dump(cnfg, config_file, sort_keys=True, indent=2) + + + @classmethod + def load(cls, path, **override_kwargs): + with open(path, "r") as config_file: + cnfg = json.load(config_file) + cnfg = munch.munchify(cnfg) + cnfg._config.update(override_kwargs) + + net = cls.from_config(cnfg._config) + net.load_weights(path.replace(".json", "_model.h5")) + + return net + + def get_weights(self): + return self._model.get_weights() + + def copy_weights_from(self, other): + assert isinstance(other, DeferredBackpropNetwork), "can only copy weights from other GrowingUNet." + + try: + self._model.set_weights(other._model.get_weights()) + except Exception as e: + LOG.error("Failed to copy weights from %s to %s", other.name, self.name) + raise e + + def save_weights(self, path): + self._model.save_weights(path) + + def load_weights(self, model_path, by_name=True): + self._model.load_weights(model_path, by_name=by_name) + + def get_config(self): + return {"name": self.name} + + @classmethod + def from_config(cls, config_dict): + return cls(**config_dict) + +class LiftingNetwork(DeferredBackpropNetwork): + def __init__(self, input_shape, output_shape, name="LiftingNetwork"): + assert isinstance(input_shape, (list, tuple)) and len(input_shape)==3 #HWC + assert isinstance(output_shape, (list, tuple)) and len(output_shape)==4 #DHWC + self.__input_shape = input_shape + self.__output_shape = output_shape + + conv2d_channels = [32,64] #OUTput channels + dense_sizes = [1024,1024,512,1024,1024] + conv3d_channels = [64,32,16] #INput channels + conv3d_channels.append(output_shape[-1]) + dense_sizes.append(np.prod(output_shape[:-1]).tolist()*conv3d_channels[0]) + + x = tf.keras.layers.Input(shape=input_shape, name=name+"_input") + inp = x + for c in conv2d_channels: + x = tf.keras.layers.Conv2D(c, 3, padding="same")(x) + x = tf.keras.layers.ReLU()(x) + + x = tf.keras.layers.Flatten()(x) + for dense_size in dense_sizes: + x = tf.keras.layers.Dense(dense_size)(x) + x = tf.keras.layers.ReLU()(x) + + x = x = tf.keras.layers.Reshape(output_shape[:-1] + [conv3d_channels[0]])(x) + for c in conv3d_channels[1:]: + x = tf.keras.layers.Conv3D(c, 3, padding="same")(x) + x = tf.keras.layers.ReLU()(x) + + model = tf.keras.Model(inputs=[inp], outputs=[x]) + super().__init__(model, name) + + def __call__(self, inputs): + if isinstance(inputs, tf.Tensor): + inputs = [inputs] + for i in range(len(inputs)): + inp = inputs[i] + rank = tf.rank(inp).numpy() + if rank==5: + inp = tf.transpose(inp, (0,2,3,1,4)) + shape = shape_list(inp) + assert shape[-2]==1 #debug + inp = tf.reshape(inp, shape[:-2] + [shape[-2]*shape[-1]]) + inputs[i] = inp + elif not rank==4: + raise RuntimeError + + assert len(inputs)==1 + assert shape_list(inputs[0])[1:]==self.__input_shape, "invaldi input shape. is: %s, required: %s"%(shape_list(inputs[0]), self.__input_shape) + + return self._model(inputs) + +def _check_make_list(value, length, _type): + if isinstance(value, (list, tuple)): + if not len(value)==length: raise ValueError("Expected %s to have length %d."%(value, length)) + if any(not isinstance(v, _type) for v in value): raise TypeError("Expected %s to have type %s, but has types %s."%(value, _type.__name__, [type(_).__name__ for _ in value])) + return list(value) + else: + if not isinstance(value, _type): raise TypeError("Expected %s to have type %s, but has type %s."%(value, _type.__name__, type(value).__name__)) + return [value]*length + +class GrowingUNet(DeferredBackpropNetwork): + @staticmethod + def get_max_levels(resolution, scale_factor, min_size=1, allow_padded=False): + # maximum number of levels the GrowingUNet can have with the current resolution + # that does not cause rounding errors when down- and up-scaling in the network + if isinstance(resolution, Iterable): + return min(GrowingUNet.get_max_levels(_, scale_factor=scale_factor, min_size=min_size, allow_padded=allow_padded) for _ in resolution) + l = 1 + while (resolution%scale_factor == 0) or (allow_padded and resolution>=min_size): + if allow_padded: + resolution = int(math.ceil(resolution/scale_factor)) + else: + resolution //= scale_factor + if resolution0 + self.num_levels = num_levels + self.__config.num_levels = num_levels + if num_levels>1: + assert isinstance(level_scale_factor, numbers.Integral) and level_scale_factor>0 + self.level_scale_factor = level_scale_factor + self.__config.level_scale_factor = level_scale_factor + + #self.num_levels if 0 #[1,self.num_levels] + assert (input_levels==-1) or (input_levels>0 and input_levels<=self.num_levels) + self.max_input_levels = input_levels + self.__config.input_levels = input_levels + self.create_inputs = create_inputs + self.__config.create_inputs = create_inputs + + #self.num_levels if 0 #[1,self.num_levels] + assert output_levels>0 and output_levels<=self.num_levels + self.max_output_levels = output_levels + self.__config.output_levels = output_levels + self._active_level = 0 + + #2 or 3 + assert dimension==2 or dimension==3 or dimension=="LIFTING" + self.dim = dimension + self.__config.dimension = dimension + assert input_channels>0 + self.input_channels = input_channels + self.__config.input_channels = input_channels + + assert conv_padding in ["ZERO", "MIRROR"] + self.padding = conv_padding #SAME MIRROR + self.__config.conv_padding = conv_padding #SAME MIRROR + + + # - Input - + assert isinstance(share_input_layer, bool) + self.share_input_layer = share_input_layer + self.__config.share_input_layer = share_input_layer + self.input_blocks = None + if input_blocks is not None: + self.input_blocks = normalize_block_config(input_blocks, num_levels=self.num_levels, is_shared=self.share_input_layer) + else: + raise NotImplementedError("input_conv_filters and input_conv_kernel_size are deprecated, use input_blocks instead.") + # if self.share_input_layer: + # assert isinstance(input_conv_filters, numbers.Integral) and input_conv_filters>0 + # self.input_conv_filters = [input_conv_filters]*self.num_levels + # assert isinstance(input_conv_kernel_size, (numbers.Integral, tuple)) + # self.input_conv_kernel_size = [input_conv_kernel_size]*self.num_levels + # else: + # self.input_conv_filters = _check_make_list(input_conv_filters, self.num_levels, numbers.Integral) + # self.input_conv_kernel_size = _check_make_list(input_conv_kernel_size, self.num_levels, numbers.Integral) + self.__config.input_blocks = self.input_blocks + # self.__config.input_conv_filters = input_conv_filters + # self.__config.input_conv_kernel_size = input_conv_kernel_size + self._input_conv_layers = {} + + + # - Encoder downsampling - + assert isinstance(share_down_layer, bool) + self.share_down_layer = share_down_layer + self.__config.share_down_layer = share_down_layer + assert down_mode in ["NONE", "AVGPOOL", "MAXPOOL", "STRIDED"] + self.down_mode = down_mode # AVGPOOL, MAXPOOL, STRIDED + if self.share_down_layer: + self.down_conv_filters = [down_conv_filters]*self.num_levels if down_conv_filters is not None else None + assert isinstance(down_conv_kernel_size, (numbers.Integral, tuple)) + self.down_conv_kernel_size = [down_conv_kernel_size]*self.num_levels + else: + self.down_conv_filters = _check_make_list(down_conv_filters, self.num_levels, numbers.Integral) if down_conv_filters is not None else None + self.down_conv_kernel_size = _check_make_list(down_conv_kernel_size, self.num_levels, numbers.Integral) + self.__config.down_conv_filters = down_conv_filters + self.__config.down_conv_kernel_size = down_conv_kernel_size + self._down_conv_layers = {} + + self.level_input_weights = {} + self._down_merge_layers = {} + + + # - Encoder - + assert isinstance(share_encoder, bool) + self.share_encoder = share_encoder + self.__config.share_encoder = share_encoder + self.encoder_resblocks = None + if encoder_resblocks is not None: + self.encoder_resblocks = normalize_block_config(encoder_resblocks, num_levels=self.num_levels, is_shared=self.share_encoder) #[encoder_resblocks]*self.num_levels if self.share_encoder else encoder_resblocks + else: + #LOG.warning("encoder_filters and encoder_kernel_sizes are deprecated, use encoder_resblocks instead.") + raise NotImplementedError("encoder_filters and encoder_kernel_sizes are deprecated, use encoder_resblocks instead.") + # if self.share_encoder: + # self.encoder_filters = [encoder_filters]*self.num_levels #list(levels) of list(enc_layers) of int + # self.encoder_kernel_sizes = [encoder_kernel_sizes]*self.num_levels #list(levels) of list(enc_layers) of int + # else: + # assert len(encoder_filters)==self.num_levels + # self.encoder_filters = encoder_filters #list(levels) of list(enc_layers) of int + # assert len(encoder_kernel_sizes)==self.num_levels + # self.encoder_kernel_sizes = encoder_kernel_sizes #list(levels) of list(enc_layers) of int or tuple + self.__config.encoder_resblocks = self.encoder_resblocks + # self.__config.encoder_filters = encoder_filters + # self.__config.encoder_kernel_sizes = encoder_kernel_sizes + self._encoder_layers = {} + + + # - Lifting - + if dimension=="LIFTING": + self.__lifting_renderer = kwargs["lifting_renderer"] + self._set_lifting_cameras(kwargs["lifting_cameras"]) + self._set_lifting_transform(kwargs["lifting_transform"]) + lifting_shapes, _ = self._compute_lifting_shapes(kwargs["lifting_shape"]) + self.__lifting_shapes = {} + self.set_lifting_shapes(lifting_shapes) + self._lifting_layers = {} + + + # - Decoder - + assert isinstance(share_decoder, bool) + self.share_decoder = share_decoder + self.__config.share_decoder = share_decoder + + self.decoder_resblocks = None + if decoder_resblocks is not None: + self.decoder_resblocks = normalize_block_config(decoder_resblocks, num_levels=self.num_levels, is_shared=self.share_decoder) #[decoder_resblocks]*self.num_levels if self.share_decoder else decoder_resblocks + else: + raise NotImplementedError("decoder_filters and decoder_kernel_sizes are deprecated, use decoder_resblocks instead.") + # if self.share_decoder: + # self.decoder_filters = [decoder_filters]*self.num_levels #list(levels) of list(dec_layers) of int + # self.decoder_kernel_sizes = [decoder_kernel_sizes]*self.num_levels #list(levels) of list(dec_layers) of int or tuple + # else: + # assert len(decoder_filters)==self.num_levels + # self.decoder_filters = decoder_filters #list(levels) of list(dec_layers) of int + # assert len(decoder_kernel_sizes)==self.num_levels + # self.decoder_kernel_sizes = decoder_kernel_sizes #list(levels) of list(dec_layers) of int or tuple + self.__config.decoder_resblocks = self.decoder_resblocks + # self.__config.decoder_filters = decoder_filters + # self.__config.decoder_kernel_sizes = decoder_kernel_sizes + self._decoder_layers = {} + + self._decoder_residual_layers = {} + self.decoder_residual_weights = {} + + + # - Decoder upsampling - + assert isinstance(share_up_layer, bool) + self.share_up_layer = share_up_layer + self.__config.share_up_layer = share_up_layer + assert up_mode in ["NNSAMPLE", "LINSAMPLE", "STRIDED", "NNSAMPLE_CONV", "LINSAMPLE_CONV"] + self.up_mode = up_mode # NNSAMPLE, LINSAMPLE, STRIDED + self.__config.up_mode = up_mode + if self.share_up_layer: + assert up_conv_filters>0 + self.up_conv_filters = [up_conv_filters]*self.num_levels + assert isinstance(up_conv_kernel_size, (numbers.Integral, tuple)) + self.up_conv_kernel_size = [up_conv_kernel_size]*self.num_levels + else: + self.up_conv_filters = _check_make_list(up_conv_filters, self.num_levels, numbers.Integral) + self.up_conv_kernel_size = _check_make_list(up_conv_kernel_size, self.num_levels, numbers.Integral) + self.__config.up_conv_filters = up_conv_filters + self.__config.up_conv_kernel_size = up_conv_kernel_size + self._up_conv_layers = {} + + assert skip_merge_mode in ["CONCAT", "WSUM", "SUM"] + self.skip_merge_mode = skip_merge_mode #CONCAT, WSUM + self.__config.skip_merge_mode = skip_merge_mode + self.level_skip_weights = {} + self._up_merge_layers = {} + + + # - Output - + assert isinstance(share_output_layer, bool) + self.share_output_layer = share_output_layer + self.__config.share_output_layer = share_output_layer + self.output_blocks = None + if output_blocks is not None: + self.output_blocks = normalize_block_config(output_blocks, num_levels=self.num_levels, is_shared=self.share_output_layer) + #else: + # LOG.warning("output_channels and output_conv_kernel_size are deprecated, use output_blocks instead.") + if self.share_output_layer: + assert isinstance(output_conv_kernel_size, (numbers.Integral, tuple)) + self.output_conv_kernel_size = [output_conv_kernel_size]*self.num_levels + else: + #assert len(output_conv_kernel_size)==self.num_levels + self.output_conv_kernel_size = _check_make_list(output_conv_kernel_size, self.num_levels, numbers.Integral) + assert isinstance(output_channels, numbers.Integral) and output_channels>0 + self.output_channels = output_channels + self._output_conv_layers = {} + self.output_activation = output_activation + assert output_mode in ["SINGLE", "RESIDUAL", "RESIDUAL_WEIGHTED"] + self.output_mode = output_mode + self.__config.output_blocks = self.output_blocks + self.__config.output_channels = output_channels + self.__config.output_conv_kernel_size = output_conv_kernel_size + self.__config.output_activation = output_activation + self.__config.output_mode = output_mode + + self.__output_slice_args = None + + self._enc_output = kwargs.get("enc_outputs", False) + if self._enc_output: + pass + + self.activation = conv_activation + self.alpha = alpha + self.__config.conv_activation = conv_activation + self.__config.alpha = alpha + + assert normalization in ["NONE", "LAYER", "LAYER_LATE", "LN", "LNL"] + self.normalization = normalization + self.__config.normalization = normalization + + assert isinstance(name, str) + self.name = name + self.__config.name = name + + self._check_config() + self.last_iteration = -1 + self.set_grow_intervals([]) + self.can_grow = False + self.input_merge_weight_schedule = None #lambda it: 0 + self.skip_merge_weight_schedule = None #lambda it: 0 + self.train_top_level_only_schedule = None #lambda it: False + self.train_mode = "ALL" + self._current_train_mode = self.train_mode + + self._build_models() + + #self._grad_acc = [tf.Variable(tf.zeros_like(tvar), trainable=False) for tvar in self.trainable_variables] persistent causes issues with growth. TODO: maybe in self.set_active_level ? + #self._build_gradient_accumulators() + self._clear_gradient_accumulators() + self.clear_gradients() + + def set_input_merge_weight(self, weight, level): + #assert 0<=level and level<(self.num_levels-1) + if not level in self.level_input_weights: raise IndexError("Level %d has no input merging stage with weight."%(level,)) + if not (0<=weight and weight<=1): raise ValueError("Input merge weight has to be in [0,1].") + # 0: only direct input from same level + # 1: only downscaled input from upper level + self.level_input_weights[level].assign(weight) + def get_input_merge_weight(self, level): + if not level in self.level_input_weights: raise IndexError("Level %d has no input merging stage with weight."%(level,)) + return self.level_input_weights[level].numpy() + + def set_skip_merge_weight(self, weight, level): + #assert 0=(self.num_levels-1): + raise RuntimeError("GrowingUNet is already at max size.") + self.set_active_level(self._active_level+1) + + def set_grow_intervals(self, intervals): + assert isinstance(intervals, (list, tuple)) + #assert len(intervals)==(self.num_levels-1) + self.grow_intervals = intervals + if False: #always start from smallest + self.grow_iterations = [0] + np.cumsum(self.grow_intervals, dtype=np.int32).tolist() + else: #always end with largest + self.grow_iterations = [0]*(self.num_levels - len(self.grow_intervals)) + np.cumsum(self.grow_intervals, dtype=np.int32).tolist() + self.can_grow = True + + + def level_start_iteration(self, level): + if level0) and (self.train_top_level_only_schedule is not None): + train_mode = self.train_mode if bool(self.train_top_level_only_schedule(iteration - self.level_start_iteration(self.current_level))) else "ALL" + if self._current_train_mode != train_mode: + LOG.debug("GrowingUNet: train mode changed from %s to %s", self._current_train_mode, train_mode) + self._current_train_mode = train_mode + rebuild_grad_acc = True + + # check growth schedule, grow if necessary + if current_level!=self.current_level: + LOG.info("GrowingUNet: set active level to %d in interation %d.", current_level, iteration) + self.set_active_level(current_level) + rebuild_grad_acc = False #already done in self.set_active_level + + # set weights by schedule + for level in range(current_level+1): + level_start_iteration = self.level_start_iteration(level) + if (level in self.level_input_weights) and (self.input_merge_weight_schedule is not None): + self.set_input_merge_weight(self.input_merge_weight_schedule(iteration - level_start_iteration), level) + if (self.skip_merge_mode!="SUM") and (level>0) and (self.skip_merge_weight_schedule is not None): + self.set_skip_merge_weight(self.skip_merge_weight_schedule(iteration - level_start_iteration), level) + #self.set_level_lr(self.level_lr_schedule(iteration - level_start_iteration), level) + + self.last_iteration = iteration + + #if rebuild_grad_acc: + # self._build_gradient_accumulators() + + def _check_config(self): + #if (self.skip_merge_mode=="CONCAT" and self.share_decoder): + # LOG.info("padding lowest level decoder input") + pass + + # --- Setup --- + + def _build_models(self): + LOG.debug("GrowingUNet: building %d models...", self.num_levels) + self.models = {} + for level in range(self.num_levels): + try: + self.models[level] = self._build_model(level) + except Exception as e: + LOG.error("Failed to build GrowingUNet level %d", level) + raise e + self.__models_built = True + + def _build_model(self, level): + LOG.debug("GrowingUNet: building level %d model", level) + inputs = []#{l: self._get_input_block(l) for l in range(level - self.max_input_levels + 1, level+1)} + inp_convs = {} + num_input_levels = (level + 1) if self.max_input_levels==-1 else self.max_input_levels + for l in range(max(level - num_input_levels + 1, 0), level+1): + inp_convs[l], inp = self._get_input_block(l) + inputs.append(inp) + inputs.reverse() + + enc_outputs = {} + # build uppermost encoder + enc_outputs[level] = self._add_encoder_block(inp_convs[level], level) + + #build lower encoders + # with inputs + for l in range(level-1, -1, -1): + if self.down_mode=="NONE": + x = inp_convs[l] + else: + #downscale + LOG.debug("Add down block level %d %s in model level %d", l, shape_list(enc_outputs[level]), level) + x = self._add_down_block(enc_outputs[l+1], l) + + #add input + if l>(level - num_input_levels): + x = self._add_encoder_input_merge_block(inp_convs[l], x, l) + + #add encoder stack + enc_outputs[l] = self._add_encoder_block(x, l) + + if self.is_lifting:# lifting + lift_outputs = {} + for l in range(level+1): + lift_outputs[l] = self._add_lifting_block(enc_outputs[l], l) + enc_outputs = lift_outputs + + dec_outputs = {} + #build lowermost decoder + if self.skip_merge_mode=="CONCAT" and self.share_decoder: + dec_outputs[0] = self._add_decoder_block(ChannelPadding((self.up_conv_filters[0],0))(enc_outputs[0]), 0) + else: + dec_outputs[0] = self._add_decoder_block(enc_outputs[0], 0) + + up_outputs = {} + #build higher decoders + for l in range(1, level+1): + #upscale + x = self._add_up_block(dec_outputs[l-1], l) + up_outputs[l] = x + + #add skip connection + x = self._add_decoder_input_merge_block(x, enc_outputs[l], l) + + #add decoder stack + dec_outputs[l] = self._add_decoder_block(x, l, up_output=up_outputs[l]) + # if self.output_mode=="RESIDUAL": + # dec_outputs[l] = tf.keras.layers.Add()([self._add_decoder_block(x, l), up_outputs[l]]) + # elif self.output_mode=="SINGLE": + # dec_outputs[l] = self._add_decoder_block(x, l) + # else: raise ValueError("Unknown output_mode '{}'".format(output_mode)) + + + #build outputs + #outputs = {l: self._add_output_block(dec_outputs[l],l) for l in range(level - self.max_output_levels + 1, level+1)} + output_levels = list(range(level - self.max_output_levels + 1, level+1)) + # if self.output_mode == "RESIDUAL": + # outputs = [] + # current_output = dec_outputs[0] + # for l in range(level+1): + # if l > 0: + # current_output = tf.keras.layers.Add()([dec_outputs[l], (tf.keras.layers.UpSampling2D if self.dim==2 else tf.keras.layers.UpSampling3D)(self.level_scale_factor)(current_output)]) + # if l in output_levels: + # outputs.append(self._add_output_block(current_output,l)) + # elif self.output_mode == "SINGLE": + outputs = [self._add_output_block(dec_outputs[l],l) for l in output_levels] + # else: raise ValueError("Unknown output_mode '{}'".format(output_mode)) + + #outputs = [self._add_output_block(dec_outputs[l],l) for l in range(level - self.max_output_levels + 1, level+1)] + outputs.reverse() + + if self._enc_output: + outputs.extend(enc_outputs[l] for l in range(level+1)) + + return tf.keras.Model(inputs=inputs, outputs=outputs) + + def __get_layer_name(self, level=0, block_name="", layer_type="", layer_idx=0): + return "{name}_l{level:d}_{block}_{type}{idx:d}".format(name=self.name, block=block_name, level=level, idx=layer_idx, type=layer_type) + + def _get_input_layers(self, level): + level = 0 if self.share_input_layer else level + return self._input_conv_layers.get(level, None) + + def _get_input_block(self, level): + input_shape = tuple([None]*self.input_dim + [self.input_channels]) + x = tf.keras.layers.Input(shape=input_shape, name=self.__get_layer_name(level, "input", "input")) + inp = x + conv_args = {} + l = 0 if self.share_input_layer else level + if l not in self._input_conv_layers: + if self.input_blocks is not None: + inp_layers = [] + for i, l_str in enumerate(self.input_blocks[l]): + idx = l_str.find(":") + 1 + inp_layers.append(layer_from_string(l_str[:idx] + "%dD_"%self.input_dim + l_str[idx:], name=self.__get_layer_name(l, "inp", "block", i), \ + activation=self.activation, alpha=self.alpha, normalization=self.normalization, padding=self.padding)) + self._input_conv_layers[l] = inp_layers + else: + self._input_conv_layers[l] = [ConvLayerND(self.input_dim, self.input_conv_filters[level], self.input_conv_kernel_size[level], 1, activation=self.activation, alpha=self.alpha, \ + padding=self.padding, normalization=self.normalization, name=self.__get_layer_name(l, "input", "conv"))] + for L in self._input_conv_layers[l]: + x = L(x) + return x, inp + + def _get_input_blocks_scale_factor(self, level): + layers = self._get_input_layers(level) + if layers is None: + raise RuntimeError("'%s' has no input layers for level %d"%(self.name, level)) + strides = [] + for layer in layers: + stride = layer.stride + if isinstance(stride, (list,tuple)): + if not all(stride[0]==_ for _ in stride): + raise ValueError("Uniform strides required for input scale factor.") + stride = stride[0] + strides.append(stride) + return np.prod(strides).tolist() + + def _get_down_layers(self, level): + level = 0 if self.share_down_layer else level + return self._down_conv_layers.get(level, None) + + def _add_down_block(self, upper_input, level): + if self.down_mode=="STRIDED": + l = 0 if self.share_down_layer else level + if l not in self._down_conv_layers: + self._down_conv_layers[l] = ConvLayerND(self.input_dim, filters=self._get_encoder_input_channels(level), kernel_size=self.down_conv_kernel_size[level], stride=self.level_scale_factor, \ + activation=self.activation, alpha=self.alpha, padding=self.padding, normalization=self.normalization, name=self.__get_layer_name(l, "down", "conv")) + x = self._down_conv_layers[l](upper_input) + elif self.down_mode=="AVGPOOL": + x = (tf.keras.layers.AveragePooling2D if self.input_dim==2 else tf.keras.layers.AveragePooling3D)(self.level_scale_factor)(upper_input) + elif self.down_mode=="MAXPOOL": + x = (tf.keras.layers.MaxPooling2D if self.input_dim==2 else tf.keras.layers.MaxPooling3D)(self.level_scale_factor)(upper_input) + else: + raise ValueError("Unknown downscale mode '%s'"%(self.down_mode,)) + return x + + def _add_encoder_input_merge_block(self, inp, down_inp, level): + if level not in self._down_merge_layers: + L = WeightedSum(0.0) + self._down_merge_layers[level] = L + self.level_input_weights[level] = L.alpha + x = self._down_merge_layers[level]([inp,down_inp]) + return x + + def _get_lifting_layer(self, level): + return self._lifting_layers.get(level, None) + + def _add_lifting_block(self, encoder_output, level): + x = encoder_output + l = level + if l not in self._lifting_layers: + self._lifting_layers[l] = UnprojectionLayer(self._get_lifting_transform(l), self._get_lifting_cameras(l), self.__lifting_renderer, name=self.__get_layer_name(l, "lifting", "unprojection")) + x = self._lifting_layers[l](x) + return x + + def __update_lifting_layers(self): + for level in range(self.num_levels): + L = self._get_lifting_layer(level) + assert isinstance(L, UnprojectionLayer) + shape = self._get_lifting_shape(level) + L.set_output_shape(shape) + + def _get_encoder_layers(self, level): + level = 0 if self.share_encoder else level + return self._encoder_layers.get(level, None) + + def _add_encoder_block(self, encoder_input, level): + x = encoder_input + l = level + if self.share_encoder: + l = 0 + if l not in self._encoder_layers: + enc_layers = [] + if self.encoder_resblocks is not None: + for i, l_str in enumerate(self.encoder_resblocks[l]): + #enc_layers.append(ResBlock.from_string("%dD_"%self.input_dim + rb_str, name=self.__get_layer_name(l, "enc", "rb", i))) + idx = l_str.find(":") + 1 + enc_layers.append(layer_from_string(l_str[:idx] + "%dD_"%self.input_dim + l_str[idx:], name=self.__get_layer_name(l, "enc", "block", i), \ + activation=self.activation, alpha=self.alpha, normalization=self.normalization, padding=self.padding)) + else: + for i, (filters, kernel_size) in enumerate(zip(self.encoder_filters[level], self.encoder_kernel_sizes[level])): + L = ConvLayerND(self.input_dim, filters, kernel_size, 1, activation=self.activation, alpha=self.alpha, padding=self.padding, normalization=self.normalization, \ + name=self.__get_layer_name(l, "enc", "conv", i)) + enc_layers.append(L) + self._encoder_layers[l] = enc_layers + + enc_layers = self._encoder_layers[l] + for L in enc_layers: + x = L(x) + return x + + def _get_decoder_layers(self, level): + level = 0 if self.share_decoder else level + return self._decoder_layers.get(level, None) + + def _add_decoder_block(self, encoder_output, level, up_output=None): + x = encoder_output + l = 0 if self.share_decoder else level + if l not in self._decoder_layers: + dec_layers = [] + if self.decoder_resblocks is not None: + for i, l_str in enumerate(self.decoder_resblocks[l]): + #dec_layers.append(ResBlock.from_string("%dD_"%self.output_dim + rb_str, name=self.__get_layer_name(l, "dec", "rb", i))) + idx = l_str.find(":") + 1 + dec_layers.append(layer_from_string(l_str[:idx] + "%dD_"%self.output_dim + l_str[idx:], name=self.__get_layer_name(l, "dec", "block", i), \ + activation=self.activation, alpha=self.alpha, normalization=self.normalization, padding=self.padding)) + else: + for i, (filters, kernel_size) in enumerate(zip(self.decoder_filters[level], self.decoder_kernel_sizes[level])): + L = ConvLayerND(self.output_dim, filters, kernel_size, 1, activation=self.activation, alpha=self.alpha, padding=self.padding, normalization=self.normalization, \ + name=self.__get_layer_name(l, "dec", "conv", i)) + dec_layers.append(L) + self._decoder_layers[l] = dec_layers + + + if self.output_mode=="SINGLE" or up_output is None: + self._decoder_residual_layers[l] = None + elif self.output_mode=="RESIDUAL": + self._decoder_residual_layers[l] = tf.keras.layers.Add() + elif self.output_mode=="RESIDUAL_WEIGHTED": + L = WeightedSum(0.5) + self._decoder_residual_layers[l] = L + self.decoder_residual_weights[l] = L.alpha + else: raise ValueError("Unknown output_mode '{}'".format(output_mode)) + + dec_layers = self._decoder_layers[l] + for L in dec_layers: + x = L(x) + if self._decoder_residual_layers[l] is not None: + x = self._decoder_residual_layers[l]([x, up_output]) + return x + + def _get_up_layers(self, level): + level = 0 if self.share_decoder else level + return self._up_conv_layers.get(level, None) + + def _add_up_block(self, lower_input, level): + assert level>0 + conv_args = {} + l = 0 if self.share_up_layer else level + + if self.up_mode=="STRIDED": + raise NotImplementedError + else: + if self.up_mode in ["NNSAMPLE", "NNSAMPLE_CONV"]: + x = (tf.keras.layers.UpSampling2D if self.output_dim==2 else tf.keras.layers.UpSampling3D)(self.level_scale_factor)(lower_input) + elif self.up_mode in ["LINSAMPLE", "LINSAMPLE_CONV"]: + if self.output_dim==2: + raise NotImplementedError("Linear upsampling only implemented for 3D.") + output_shape = [int(_*self.level_scale_factor) for _ in shape_list(lower_input)[-4:-1]] + x = GridSamplingLayer(output_shape)(lower_input) + else: + raise ValueError("Unknown upscale mode '%s'"%(self.up_mode,)) + if self.up_mode in ["NNSAMPLE_CONV", "LINSAMPLE_CONV"]: + if l not in self._up_conv_layers: + self._up_conv_layers[l] = ConvLayerND(self.output_dim, self.up_conv_filters[level], self.up_conv_kernel_size[level], 1, activation=self.activation, alpha=self.alpha, \ + padding=self.padding, normalization=self.normalization, name=self.__get_layer_name(l, "up", "conv")) + x = self._up_conv_layers[l](x) + return x + + def _add_decoder_input_merge_block(self, up_inp, skip_inp, level): + if self.skip_merge_mode=="CONCAT": + if level not in self.level_skip_weights: + #self.level_skip_weights[level] = tf.keras.backend.variable(0.0, dtype="float32", name="skip_weight") + #self._up_merge_layers[level] = tf.keras.layers.Lambda(lambda t: t * self.level_skip_weights[level], name=self.__get_layer_name(level, "skip", "weighting")) + self._up_merge_layers[level] = ScalarMul(0.0, name=self.__get_layer_name(level, "skip", "weighting")) + self.level_skip_weights[level] = self._up_merge_layers[level].alpha + #self.level_skip_weights[level] = skip_weight + #skip_weight = self.level_skip_weights[level] + y = self._up_merge_layers[level](skip_inp) #tf.keras.layers.Multiply()([skip_inp, [skip_weight]]) + x = tf.keras.layers.Concatenate(axis=-1)([up_inp,y]) + elif self.skip_merge_mode=="WSUM": + if level not in self._up_merge_layers: + L = WeightedSum(0.0) + self._up_merge_layers[level] = L + self.level_skip_weights[level] = L.alpha + L = self._up_merge_layers[level] + x = L([up_inp, skip_inp]) + elif self.skip_merge_mode=="SUM": + if level not in self._up_merge_layers: + L = tf.keras.layers.Add() + self._up_merge_layers[level] = L + L = self._up_merge_layers[level] + x = L([up_inp, skip_inp]) + else: + raise ValueError("Unknown skip connection merge mode '%s'"%(self.skip_merge_mode,)) + return x + + def _get_output_layers(self, level): + level = 0 if self.share_output_layer else level + return self._output_conv_layers.get(level, None) + + def _add_output_block(self, decoder_output, level): + x = decoder_output + l = 0 if self.share_output_layer else level + if l not in self._output_conv_layers: + outp_layers = [] + if self.output_blocks is not None: + for i, l_str in enumerate(self.output_blocks[l]): + idx = l_str.find(":") + 1 + outp_layers.append(layer_from_string(l_str[:idx] + "%dD_"%self.output_dim + l_str[idx:], name=self.__get_layer_name(l, "outp", "block", i), \ + activation=self.activation, alpha=self.alpha, normalization=self.normalization, padding=self.padding)) + + if self.output_conv_kernel_size[level]>0: + outp_layers.append(ConvLayerND(self.output_dim, self.output_channels, self.output_conv_kernel_size[level], 1, activation=self.output_activation, \ + alpha=self.alpha, padding=self.padding, name=self.__get_layer_name(l, "output", "conv"))) + self._output_conv_layers[l] = outp_layers + for L in self._output_conv_layers[l]: + x = L(x) + return x + + def _get_encoder_input_channels(self, level): + if self.down_conv_filters is None: + level = 0 if self.share_input_layer else level + if self.input_blocks is not None: + return self._input_conv_layers[level][-1].num_output_channels() + else: + return self.input_conv_filters[level] + else: + return self.down_conv_filters[level] + #return self.input_conv_filters[level] if self.down_conv_filters is None else self.down_conv_filters[level] + def _get_encoder_output_channels(self, level): + if self.share_encoder: + level = 0 + return self._encoder_layers[level][-1].num_output_channels() + #return self.encoder_filters[level][-1] + def _get_decoder_input_channels(self, level): + if self.skip_merge_mode in ["WSUM", "SUM"] or level==0: + return self._get_encoder_output_channels(level) + else: + return self._get_encoder_output_channels(level) + (self.up_conv_filters[level] if self.up_mode=="STRIDED" else self._get_decoder_output_channels(level)) + def _get_decoder_output_channels(self, level): + return self.decoder_filters[level][-1] + + def _create_scaled_inputs(self, base_input): + inputs = [base_input] + for i in range(1, self.num_inputs): + inputs.append(self._scale_tensor_down(inputs[i-1], self.level_scale_factor)) + return inputs + + def _scale_tensor_down(self, tensor, scale): + if self.input_dim==2: + return tf.nn.avg_pool(tensor, (1,scale,scale,1), (1,scale,scale,1), "SAME") + elif self.input_dim==3: + return tf.nn.avg_pool3d(tensor, (1,scale,scale,scale,1), (1,scale,scale,scale,1), "SAME") + else: + raise ValueError + #return tf_image_resize_mip(tensor, size, mip_bias=0.5, method=tf.image.ResizeMethod.BILINEAR) + + @property + def num_inputs(self): + return (self._active_level+1) if self.max_input_levels==-1 else min(self.max_input_levels, self._active_level+1) + @property + def num_outputs(self): + return (self._active_level+1) if self.max_output_levels==-1 else min(self.max_output_levels, self._active_level+1) + + def get_scale(self, level=None): + if level is None: + level = self.current_level + return self.level_scale_factor ** level + + def check_inputs(self, inputs): + if not isinstance(inputs, (list, tuple)) or not all(isinstance(inp, tf.Tensor) for inp in inputs): + raise TypeError("Inputs must be list of tf.Tensor") + if not len(inputs)==self.num_inputs: + raise ValueError("Expected %d inputs for '%s', got %d"%(self.num_inputs, self.name, len(inputs))) + last_shape = None + for i, inp in enumerate(inputs): + shape = shape_list(inp) + if not len(shape)==(self.input_dim+2): + raise ValueError("Input %d of '%s' has wrong rank. expected %d, is %d %s"%(i, self.name, self.input_dim+2, len(shape), shape)) + c = shape[-1] + if c != self.input_channels: + raise ValueError("Input %d of '%s' has %d channels, but %d are required."%(i, self.name, c, self.input_channels)) + if last_shape is not None and not last_shape==[_*self.level_scale_factor for _ in shape[-(self.input_dim+1):-1]]: + raise ValueError("Input %d of '%s' has shape %s, but %s is required for scale factor %d."%(i, self.name, shape[-(self.input_dim+1):-1], [_//self.level_scale_factor for _ in last_shape])) + last_shape = shape[-(self.input_dim+1):-1] + + def _get_current_padding_scale_factor(self, level=None): + if level is None: level = self.get_active_level() + input_factor = self._get_input_blocks_scale_factor(level) + unet_factor = self.level_scale_factor**(level) + return input_factor * unet_factor + + def _get_padded_input_shape(self, input_shape, level=None): + assert isinstance(input_shape, (list, tuple)) and len(input_shape)==3 and all(isinstance(_, numbers.Integral) for _ in input_shape) + level = level if level is not None else self.get_active_level() + scale_factor = self._get_current_padding_scale_factor(level) + if scale_factor==1: + return list(input_shape) + return [next_div_by(_, scale_factor) for _ in input_shape] + + def _pad_inputs(self, inputs): + assert len(inputs)==1 + level = self.get_active_level() + scale_factor = self._get_current_padding_scale_factor(level) + if scale_factor==1: + return inputs + inp, pad = tf_pad_to_next_div_by(inputs[0], div=scale_factor, pad_axes=list(range(1,self.input_dim+1)), return_paddings=True) + slice_args = {"begin": [_[0] for _ in pad], "size": shape_list(inputs[0])} + return [inp], slice_args + + def _cut_outputs(self, outputs, begin, size): + # need to adjust for potentially different amount of channels + + if isinstance(outputs, (list, tuple)): + output_list = True + outputs = outputs[0] + else: + output_list = False + shape = shape_list(outputs) + assert len(shape)==5 + + size = list(size) + if len(size)==5: + size[-1] = shape[-1] + elif len(size)==3: + size = [shape[0]] + size + [shape[-1]] + else: + raise ValueError + + begin = list(begin) + if len(begin)==5: + pass + elif len(begin)==3: + begin = [0] + begin + [0] + else: + raise ValueError + + outputs = tf.slice(outputs, begin=begin, size=size) + if not has_shape(outputs, size): + raise ValueError("Expected cut output of active level %d to have shape %s, got %s"%(self.get_active_level(), size, shape_list(outputs),)) + if output_list: + outputs = [outputs] + + return outputs + + def check_outputs(self, outputs): + if isinstance(outputs, tf.Tensor): + outputs = [outputs] + if not isinstance(outputs, (list, tuple)) or not all(isinstance(outp, tf.Tensor) for outp in outputs): + raise TypeError("Internal: Outputs must be list of tf.Tensor") + if not len(outputs)==self.num_outputs: + raise ValueError("Internal: Expected %d outputs for '%s', got %d"%(self.num_outputs, self.name, len(outputs))) + + if self.is_lifting: + level = self.get_active_level() + lifting_shape = self._get_lifting_shape(level) + if not has_shape(outputs[-1], [None]+lifting_shape+[None]): + raise ValueError("Expected output of active level %d to have shape %s, got %s"%(level, [None]+lifting_shape+[None], shape_list(outputs[-1]),)) + + def __call__(self, inputs): + with SAMPLE("call %s lvl %d"%(self.name[:10], self._active_level)): + if self.create_inputs: + assert isinstance(inputs, tf.Tensor) + inputs = self._create_scaled_inputs(inputs) + elif isinstance(inputs, tf.Tensor): + inputs = [inputs] + LOG.debug("GrowingUNet: called '%s' with inputs: %s", self.name, [shape_list(_) for _ in inputs]) + + padded_input = False + if not self.is_lifting: + self.__output_slice_args = None + if self.num_inputs==1 and self.num_levels>1 and self.num_outputs==1: + padded_input = True + shape1 = shape_list(inputs[0]) + inputs, output_slice_args = self._pad_inputs(inputs) + level = self.get_active_level() + if not self.is_lifting and self._get_input_blocks_scale_factor(level)==1: + self.__output_slice_args = output_slice_args + shape2 = shape_list(inputs[0]) + LOG.debug("GrowingUNet: padded input of '%s' from %s to %s for %d levels with scale factor %d", self.name, shape1, shape2, self.get_active_level()+1, self.level_scale_factor) + + self.check_inputs(inputs) + #inputs = {l: inputs[self._active_level - l] for l in range(self._active_level, self._active_level-self.num_inputs, -1)} + with SAMPLE("model"): + outputs = self.models[self._active_level](inputs) + + if self._enc_output: + enc_outputs = outputs[self.num_outputs:] + outputs = outputs[:self.num_outputs] + if len(outputs)==1: outputs = outputs[0] + #outputs = [v for k,v in sorted(outputs.items(), key=lambda k,v:k, reverse=True)] + #LOG.info("%s output shape: %s", self.name, shape_list(outputs)) + self.check_outputs(outputs) + if self.__output_slice_args is not None: + outputs = self._cut_outputs(outputs, **self.__output_slice_args) + + if self._enc_output: + return outputs, enc_outputs + else: + return outputs + + def get_layers(self, level): + layers = [] + + L = self._get_input_layers(level) + if L is not None: layers.extend(L) + L = self._get_down_layers(level) + if L is not None: layers.append(L) + + L = self._get_encoder_layers(level) + if L is not None: layers.extend(L) + L = self._get_decoder_layers(level) + if L is not None: layers.extend(L) + + L = self._get_up_layers(level) + if L is not None: layers.append(L) + L = self._get_output_layers(level) + if L is not None: layers.extend(L) + + return layers + + def get_trainable_variables(self, level): + layers = self.get_layers(level) + layer_vars = [] + for L in layers: layer_vars += L.trainable_variables + return layer_vars + + def get_layer_decoder(self, level): + layers = [] + + L = self._get_decoder_layers(level) + if L is not None: layers.extend(L) + + L = self._get_up_layers(level) + if L is not None: layers.append(L) + L = self._get_output_layers(level) + if L is not None: layers.extend(L) + + return layers + + + def get_trainable_variables_decoder(self, level): + layers = self.get_layer_decoder(level) + layer_vars = [] + for L in layers: layer_vars += L.trainable_variables + return layer_vars + + @property + def current_level(self): + return self._active_level + + @property + def trainable_variables(self): + # TODO: return + # how to handle weight sharing with different learning rates? + if self._current_train_mode == "TOP": #top_level_only: + return self.get_trainable_variables(self._active_level) + elif self._current_train_mode == "TOP_DEC": #top_level_only: + return self.get_trainable_variables_decoder(self._active_level) + elif self._current_train_mode == "ALL": + return self.models[self._active_level].trainable_variables + else: + raise ValueError("Unknown train mode: %s"(self._current_train_mode,)) + + + def get_grads_vars_by_level(self, keep_gradients=True, normalize=False): + active_levels = tuple(range(self._active_level+1)) + if self.is_frozen_weights or not self.has_pending_gradients: + #raise RuntimeError("'%s' does not have any recorded gradients."%(self.name,)) + return [tuple() for _ in active_levels] + + #DEBUG + #normalize=True + grads_vars = [] + for level in active_levels: + vars_level = self.get_trainable_variables(level) + if normalize: + s = self._get_gradient_normalization_factor() + grads_level = [tf.identity(self._get_gradient_accumulator(var))*s for var in vars_level] + else: + grads_level = [tf.identity(self._get_gradient_accumulator(var)) for var in vars_level] + + grads_vars_level = tuple(zip(grads_level, vars_level)) + grads_vars.append(grads_vars_level) + + + # can clear gradients here as we return a copy + if not keep_gradients: + self.clear_gradients() + + return grads_vars + + # def add_gradients(self, grads, allow_none=False): + # with SAMPLE("{} add grads".format(self.name)): + # for i, (acc, grad) in enumerate(zip(self._grad_acc, grads)): + # if grad is not None: + # acc.assign_add(grad) + # self._pending_gradients = True + # elif not allow_none: + # raise ValueError("{} gradient #{} is None.".format(self.name, i)) + + # def apply_gradients(self, optimizer, keep_gradients=False): + # with SAMPLE("{} apply grads".format(self.name)): + # if not self.has_pending_gradients: + # raise RuntimeError("GrowingUNet '%s' does not have any recorded gradients to apply."%(self.name,)) + # grads_vars = zip(self._grad_acc, self.trainable_variables) + # optimizer.apply_gradients(grads_vars) + # if not keep_gradients: + # self.clear_gradients() + + # def _build_gradient_accumulators(self): + # self._grad_acc = [tf.Variable(tf.zeros_like(tvar), trainable=False) for tvar in self.trainable_variables] + # self._pending_gradients = False + + + # def clear_gradients(self): + # with SAMPLE("{} clear grads".format(self.name)): + # #https://stackoverflow.com/questions/46772685/how-to-accumulate-gradients-in-tensorflow + # for acc in self._grad_acc: + # acc.assign(tf.zeros_like(acc)) + # self._pending_gradients = False + + # @property + # def has_pending_gradients(self): + # return self._pending_gradients + + # def get_pending_gradients(self): + # return [tf.identity(_) for _ in self._grad_acc] + + # def get_pending_gradients_summary(self): + # return [[tf.reduce_mean(_).numpy(), tf.reduce_max(tf.abs(_)).numpy()] for _ in self._grad_acc] + + # def get_weights_summary(self): + # return [[tf.reduce_mean(_).numpy(), tf.reduce_max(tf.abs(_)).numpy()] for _ in self.trainable_variables] + + # def compute_regularization_gradients(self, weight=1.0, add_gradients=False): + # weights = self.trainable_variables + # with tf.GradientTape(persistent=True, watch_accessed_variables=False) as tape: + # tape.watch(weights) + # #loss = tf.reduce_mean([tf.reduce_mean(tf.nn.l2_loss(var)) for var in weights]) * weight + # loss = tf.reduce_sum([tf.reduce_sum(tf.nn.l2_loss(var)) for var in weights]) * weight + # grads = tape.gradient(loss, weights) + # if add_gradients: + # self.add_gradients(grads) + # return grads + + def summary(self, print_fn=print): + self.models[self.num_levels-1].summary(print_fn=print_fn) + + def save(self, path): + model_path = path + path = path.replace("_model.h5",".json") + self.models[self.num_levels-1].save(model_path) + + cnfg = munch.Munch() + cnfg._config = self.get_config() + cnfg._state = munch.Munch() + cnfg._state.level_skip_weights = {l:v.numpy().tolist() for l,v in self.level_skip_weights.items()} + cnfg._state.level_input_weights = {l:v.numpy().tolist() for l,v in self.level_input_weights.items()} + cnfg._state.active_level = self._active_level + #cnfg._state.model_path = model_path + + with open(path, "w") as config_file: + json.dump(cnfg, config_file, sort_keys=True, indent=2) + + + @classmethod + def load(cls, path, **override_kwargs): + with open(path, "r") as config_file: + cnfg = json.load(config_file) + cnfg = munch.munchify(cnfg) + cnfg._config.update(override_kwargs) + net = cls.from_config(cnfg._config) + + net_max_level = net.num_levels-1 + net.load_weights(path.replace(".json", "_model.h5")) + net.set_active_level(min(cnfg._state.active_level, net_max_level)) + for l, v in cnfg._state.level_input_weights.items(): + if int(l)<=net_max_level: + net.set_input_merge_weight(v,int(l)) + for l, v in cnfg._state.level_skip_weights.items(): + if int(l)<=net_max_level: + net.set_skip_merge_weight(v,int(l)) + + return net + + def get_weights(self, level=None): + if level is None: + level = self.num_levels-1 + return self.models[level].get_weights() + + def copy_weights_from(self, other): + assert isinstance(other, GrowingUNet), "can only copy weights from other GrowingUNet." + # check compatibility + assert self.num_levels==other.num_levels + + # set weights of models + for level in range(self.num_levels): + try: + self.models[level].set_weights(other.models[level].get_weights()) + except Exception as e: + LOG.error("Failed to copy weights from level %d of %s to %s", level, other.name, self.name) + raise e + + def save_weights(self, path): + self.models[self.num_levels-1].save_weights(path) + + def load_weights(self, model_path, by_name=True): + for level, model in self.models.items(): + model.load_weights(model_path, by_name=by_name) + + def get_config(self): + return copy.deepcopy(self.__config) + + @classmethod + def from_config(cls, config_dict): + return cls(**config_dict) + + @staticmethod + def config_is_level_variable(config): + if not isinstance(config, dict): + #assume path + with open(config, "r") as config_file: + config = json.load(config_file) + config = config["_config"] + config = munch.munchify(config) + + return config.share_decoder \ + and config.share_down_layer \ + and config.share_encoder \ + and config.share_input_layer \ + and config.share_output_layer \ + and config.share_up_layer + + +class SDFDiffAENetwork(DeferredBackpropNetwork): + def __init__(self, input_channels, name="SDFDiffAENetwork"): + input_shape = [64,64,input_channels] + + x = tf.keras.layers.Input(shape=input_shape, name=name+"_input") + inp = x + + # Encoder + + # res=64 + x = tf.keras.layers.Conv2D(filters=64, kernel_size=5, strides=2, padding="same")(x) + x = tf.keras.layers.BatchNormalization(axis=-1)(x) + x = tf.keras.layers.ReLU()(x) + # res=32 + x = tf.keras.layers.Conv2D(filters=128, kernel_size=5, strides=2, padding="same")(x) + x = tf.keras.layers.BatchNormalization(axis=-1)(x) + x = tf.keras.layers.ReLU()(x) + # res=16 + x = tf.keras.layers.Conv2D(filters=256, kernel_size=5, strides=2, padding="same")(x) + x = tf.keras.layers.BatchNormalization(axis=-1)(x) + x = tf.keras.layers.ReLU()(x) + # res=8 + x = tf.keras.layers.Flatten()(x) + x = tf.keras.layers.Dense(1024)(x) + x = tf.keras.layers.ReLU()(x) + + x = tf.keras.layers.Dense(1024)(x) + x = tf.keras.layers.ReLU()(x) + + x = tf.keras.layers.Dense(512)(x) + x = tf.keras.layers.ReLU()(x) + + # Decoder + + x = tf.keras.layers.Dense(1024)(x) + x = tf.keras.layers.ReLU()(x) + + x = tf.keras.layers.Dense(1024)(x) + x = tf.keras.layers.ReLU()(x) + + x = tf.keras.layers.Dense(32*4*4*4)(x) + x = tf.keras.layers.ReLU()(x) + + x = tf.keras.layers.Dense(64*8*8*8)(x) + x = tf.keras.layers.ReLU()(x) + x = tf.keras.layers.Reshape((8,8,8,64))(x) + # res=8 + + # Dense block 1 + d1_0 = x + x = tf.keras.layers.Conv3DTranspose(filters=32, kernel_size=3, strides=1, padding="same")(x) + x = tf.keras.layers.BatchNormalization(axis=-1)(x) + x = tf.keras.layers.ReLU()(x) + d1_1 = x + + x = tf.keras.layers.Concatenate(axis=-1)([d1_0, d1_1]) + x = tf.keras.layers.Conv3DTranspose(filters=32, kernel_size=3, strides=1, padding="same")(x) + x = tf.keras.layers.BatchNormalization(axis=-1)(x) + x = tf.keras.layers.ReLU()(x) + d1_2 = x + + x = tf.keras.layers.Concatenate(axis=-1)([d1_0, d1_1, d1_2]) + x = tf.keras.layers.Conv3DTranspose(filters=32, kernel_size=3, strides=1, padding="same")(x) + x = tf.keras.layers.BatchNormalization(axis=-1)(x) + x = tf.keras.layers.ReLU()(x) + d1_3 = x + + x = tf.keras.layers.Concatenate(axis=-1)([d1_0, d1_1, d1_2, d1_3]) + x = tf.keras.layers.Conv3DTranspose(filters=64, kernel_size=1, strides=1, padding="same")(x) + x = tf.keras.layers.BatchNormalization(axis=-1)(x) + x = tf.keras.layers.ReLU()(x) + + # upscale + x = tf.keras.layers.Conv3DTranspose(filters=32, kernel_size=4, strides=2, padding="same")(x) + x = tf.keras.layers.BatchNormalization(axis=-1)(x) + x = tf.keras.layers.ReLU()(x) + # res=16 + + # Dense block 2 + d2_0 = x + x = tf.keras.layers.Conv3DTranspose(filters=16, kernel_size=3, strides=1, padding="same")(x) + x = tf.keras.layers.BatchNormalization(axis=-1)(x) + x = tf.keras.layers.ReLU()(x) + d2_1 = x + + x = tf.keras.layers.Concatenate(axis=-1)([d2_0, d2_1]) + x = tf.keras.layers.Conv3DTranspose(filters=16, kernel_size=3, strides=1, padding="same")(x) + x = tf.keras.layers.BatchNormalization(axis=-1)(x) + x = tf.keras.layers.ReLU()(x) + d2_2 = x + + x = tf.keras.layers.Concatenate(axis=-1)([d2_0, d2_1, d2_2]) + x = tf.keras.layers.Conv3DTranspose(filters=16, kernel_size=3, strides=1, padding="same")(x) + x = tf.keras.layers.BatchNormalization(axis=-1)(x) + x = tf.keras.layers.ReLU()(x) + d2_3 = x + + x = tf.keras.layers.Concatenate(axis=-1)([d2_0, d2_1, d2_2, d2_3]) + x = tf.keras.layers.Conv3DTranspose(filters=32, kernel_size=1, strides=1, padding="same")(x) + x = tf.keras.layers.BatchNormalization(axis=-1)(x) + x = tf.keras.layers.ReLU()(x) + + # upscale + x = tf.keras.layers.Conv3DTranspose(filters=16, kernel_size=4, strides=2, padding="same")(x) + x = tf.keras.layers.BatchNormalization(axis=-1)(x) + x = tf.keras.layers.ReLU()(x) + # res=32 + + # Dense block 3 + d3_0 = x + x = tf.keras.layers.Conv3DTranspose(filters=8, kernel_size=3, strides=1, padding="same")(x) + x = tf.keras.layers.BatchNormalization(axis=-1)(x) + x = tf.keras.layers.ReLU()(x) + d3_1 = x + + x = tf.keras.layers.Concatenate(axis=-1)([d3_0, d3_1]) + x = tf.keras.layers.Conv3DTranspose(filters=8, kernel_size=3, strides=1, padding="same")(x) + x = tf.keras.layers.BatchNormalization(axis=-1)(x) + x = tf.keras.layers.ReLU()(x) + d3_2 = x + + x = tf.keras.layers.Concatenate(axis=-1)([d3_0, d3_1, d3_2]) + x = tf.keras.layers.Conv3DTranspose(filters=8, kernel_size=3, strides=1, padding="same")(x) + x = tf.keras.layers.BatchNormalization(axis=-1)(x) + x = tf.keras.layers.ReLU()(x) + d3_3 = x + + x = tf.keras.layers.Concatenate(axis=-1)([d3_0, d3_1, d3_2, d3_3]) + x = tf.keras.layers.Conv3DTranspose(filters=16, kernel_size=1, strides=1, padding="same")(x) + x = tf.keras.layers.BatchNormalization(axis=-1)(x) + x = tf.keras.layers.ReLU()(x) + + x = tf.keras.layers.Conv3DTranspose(filters=8, kernel_size=3, strides=1, padding="same")(x) + x = tf.keras.layers.BatchNormalization(axis=-1)(x) + x = tf.keras.layers.ReLU()(x) + + # upscale + x = tf.keras.layers.Conv3DTranspose(filters=1, kernel_size=4, strides=2, padding="same")(x) + # res=64 + self.output_channels = 1 + + model = tf.keras.Model(inputs=[inp], outputs=[x]) + super().__init__(model, name) + + def __call__(self, inputs): + + x = self._model(inputs) + x = tf.math.sigmoid(x) * 4 - 2 + return x + + +class SDFDiffRefinerNetwork(DeferredBackpropNetwork): + def __init__(self, name="SDFDiffRefinerNetwork"): + input_shape = [64,64,64,1] + + x = tf.keras.layers.Input(shape=input_shape, name=name+"_input") + inp = x + + # MaxPool3D gradients are broken in my version... + pool_fn = tf.keras.layers.AvgPool3D #tf.keras.layers.MaxPool3D + + # Down + # res=64 + l0 = x + x = tf.keras.layers.Conv3D(filters=32, kernel_size=4, strides=1, padding="same")(x) + x = tf.keras.layers.BatchNormalization(axis=-1)(x) + x = tf.keras.layers.LeakyReLU(0.2)(x) + x = pool_fn((2,2,2))(x) + # res=32 + l1 = x + x = tf.keras.layers.Conv3D(filters=64, kernel_size=4, strides=1, padding="same")(x) + x = tf.keras.layers.BatchNormalization(axis=-1)(x) + x = tf.keras.layers.LeakyReLU(0.2)(x) + x = pool_fn((2,2,2))(x) + # res=16 + l2 = x + x = tf.keras.layers.Conv3D(filters=128, kernel_size=4, strides=1, padding="same")(x) + x = tf.keras.layers.BatchNormalization(axis=-1)(x) + x = tf.keras.layers.LeakyReLU(0.2)(x) + x = pool_fn((2,2,2))(x) + # res=8 + l3 = x + x = tf.keras.layers.Conv3D(filters=256, kernel_size=4, strides=1, padding="same")(x) + x = tf.keras.layers.BatchNormalization(axis=-1)(x) + x = tf.keras.layers.LeakyReLU(0.2)(x) + x = pool_fn((2,2,2))(x) + # res=4 + l4 = x + + #Latent + x = tf.keras.layers.Flatten()(x) + x = tf.keras.layers.Dense(2048)(x) + x = tf.keras.layers.ReLU()(x) + + x = tf.keras.layers.Dense(8192*2)(x) + x = tf.keras.layers.ReLU()(x) + x = tf.keras.layers.Reshape((4,4,4,256))(x) + + # Up + x = tf.keras.layers.Add()([l4, x]) + x = tf.keras.layers.Conv3DTranspose(filters=128, kernel_size=4, strides=2, padding="same")(x) + x = tf.keras.layers.BatchNormalization(axis=-1)(x) + x = tf.keras.layers.ReLU()(x) + # res=8 + x = tf.keras.layers.Add()([l3, x]) + x = tf.keras.layers.Conv3DTranspose(filters=64, kernel_size=4, strides=2, padding="same")(x) + x = tf.keras.layers.BatchNormalization(axis=-1)(x) + x = tf.keras.layers.ReLU()(x) + # res=16 + x = tf.keras.layers.Add()([l2, x]) + x = tf.keras.layers.Conv3DTranspose(filters=32, kernel_size=4, strides=2, padding="same")(x) + x = tf.keras.layers.BatchNormalization(axis=-1)(x) + x = tf.keras.layers.ReLU()(x) + # res=32 + x = tf.keras.layers.Add()([l1, x]) + x = tf.keras.layers.Conv3DTranspose(filters=1, kernel_size=4, strides=2, padding="same")(x) + # res=64 + #x = tf.keras.layers.Add()([l1, x]) + + model = tf.keras.Model(inputs=[inp], outputs=[x]) + super().__init__(model, name) + + def __call__(self, inputs): + + x = self._model(inputs) + x = tf.math.sigmoid(x) + x = (x + inputs) * 0.5 + return x + + +class RWDensityGeneratorNetwork(DeferredBackpropNetwork): + def __init__(self, input_channels, w1=0.5, w2=0.5, name="RWDensityGeneratorNetwork"): + input_shape = [64,64,64,input_channels] + self._single_view = w2==0 + + shared_RB0 = ResBlock(dim=3, mid_filters= 8, out_filters= 8, mid0_kernel_size=3, mid1_kernel_size=3, skip_kernel_size=1, stride=1, activation="lrelu", alpha=0.2, name=name+"_shared_RB0") + shared_RB1 = ResBlock(dim=3, mid_filters= 8, out_filters= 8, mid0_kernel_size=3, mid1_kernel_size=3, skip_kernel_size=0, stride=1, activation="lrelu", alpha=0.2, name=name+"_shared_RB1") + shared_RB2 = ResBlock(dim=3, mid_filters=16, out_filters=16, mid0_kernel_size=3, mid1_kernel_size=3, skip_kernel_size=1, stride=1, activation="lrelu", alpha=0.2, name=name+"_shared_RB2") + shared_RB3 = ResBlock(dim=3, mid_filters=16, out_filters=16, mid0_kernel_size=3, mid1_kernel_size=3, skip_kernel_size=0, stride=1, activation="lrelu", alpha=0.2, name=name+"_shared_RB3") + shared_RB4 = ResBlock(dim=3, mid_filters=32, out_filters=32, mid0_kernel_size=3, mid1_kernel_size=3, skip_kernel_size=1, stride=1, activation="lrelu", alpha=0.2, name=name+"_shared_RB4") + shared_RB5 = ResBlock(dim=3, mid_filters=32, out_filters=32, mid0_kernel_size=3, mid1_kernel_size=3, skip_kernel_size=0, stride=1, activation="lrelu", alpha=0.2, name=name+"_shared_RB5") + + inp1 = tf.keras.layers.Input(shape=input_shape, name=name+"_input1") + x1 = inp1 + x1 = shared_RB0(x1) + l0_1 = x1 + x1 = shared_RB1(x1) + x1 = shared_RB2(x1) + x1 = shared_RB3(x1) + x1 = shared_RB4(x1) + x1 = shared_RB5(x1) + x1 = ScalarMul(w1, name=name+"_weightEnc1")(x1) #tf.keras.layers.Multiply()([w1, x1]) + + if not self._single_view: + inp2 = tf.keras.layers.Input(shape=input_shape, name=name+"_input2") + inp = [inp1,inp2] + x2 = inp2 + x2 = shared_RB0(x2) + l0_2 = x2 + x2 = shared_RB1(x2) + x2 = shared_RB2(x2) + x2 = shared_RB3(x2) + x2 = shared_RB4(x2) + x2 = shared_RB5(x2) + x2 = ScalarMul(w2, name=name+"_weightEnc2")(x2) #tf.keras.layers.Multiply()([w2, x2]) + + x = tf.keras.layers.Concatenate(axis=-1, name=name+"_concatEnc")([x1, x2]) + else: + inp = [inp1] + x = x1 + + x = ResBlock(dim=3, mid_filters=16, out_filters=16, mid0_kernel_size=3, mid1_kernel_size=3, skip_kernel_size=1, stride=1, activation="lrelu", alpha=0.2, name=name+"_RB6")(x) + x = ResBlock(dim=3, mid_filters=8, out_filters=8, mid0_kernel_size=3, mid1_kernel_size=3, skip_kernel_size=1, stride=1, activation="lrelu", alpha=0.2, name=name+"_RB7")(x) + + x = tf.keras.layers.Concatenate(axis=-1, name=name+"_concatSkip")([l0_1, x] if self._single_view else [l0_1, l0_2, x]) + + x = ResBlock(dim=3, mid_filters=1, out_filters=1, mid0_kernel_size=3, mid1_kernel_size=3, skip_kernel_size=1, stride=1, activation="lrelu", alpha=0.2, name=name+"_RB8")(x) + + x = tf.keras.layers.Add(name=name+"_addSkip")([x, ScalarMul(w1, name=name+"_weightInp1")(inp1)] if self._single_view else [x, ScalarMul(w1, name=name+"_weightInp1")(inp1), ScalarMul(w2, name=name+"_weightInp2")(inp2)]) + + model = tf.keras.Model(inputs=inp, outputs=[x]) + super().__init__(model, name) + + def __call__(self, inputs): + # split concatenated inputs + if not self._single_view: + inputs = tf.split(inputs, 2, axis=-1) + + x = self._model(inputs) + return x + +class RWVelocityGeneratorNetwork(DeferredBackpropNetwork): + def __init__(self, dens_channels, unp_channels, use_proxy=False, name="RWVelocityGeneratorNetwork"): + dens_shape = [64,64,64,dens_channels] + unp_shape = [64,64,64,unp_channels] + + self._use_proxy = use_proxy + + if self._use_proxy: + inp = [ + tf.keras.layers.Input(shape=dens_shape, name=name+"_inputDens0"), + tf.keras.layers.Input(shape=dens_shape, name=name+"_inputDens1"), + ] + else: + inp = [ + tf.keras.layers.Input(shape=dens_shape, name=name+"_inputDens0"), + tf.keras.layers.Input(shape=unp_shape, name=name+"_inputUnp0"), + tf.keras.layers.Input(shape=unp_shape, name=name+"_inputUnp1"), + ] + + x = tf.keras.layers.Concatenate(axis=-1)(inp) + + x = ResBlock(dim=3, mid_filters=16, out_filters=16, mid0_kernel_size=3, mid1_kernel_size=3, skip_kernel_size=1, stride=1, activation="lrelu", alpha=0.2, name=name+"_RB0")(x) + x = ResBlock(dim=3, mid_filters=32, out_filters=32, mid0_kernel_size=3, mid1_kernel_size=3, skip_kernel_size=1, stride=1, activation="lrelu", alpha=0.2, name=name+"_RB1")(x) + x = ResBlock(dim=3, mid_filters=48, out_filters=48, mid0_kernel_size=3, mid1_kernel_size=3, skip_kernel_size=1, stride=1, activation="lrelu", alpha=0.2, name=name+"_RB2")(x) + x = ResBlock(dim=3, mid_filters=16, out_filters=16, mid0_kernel_size=3, mid1_kernel_size=3, skip_kernel_size=1, stride=1, activation="lrelu", alpha=0.2, name=name+"_RB3")(x) + x = ResBlock(dim=3, mid_filters= 3, out_filters= 3, mid0_kernel_size=3, mid1_kernel_size=3, skip_kernel_size=1, stride=1, activation="lrelu", alpha=0.2, name=name+"_RB4")(x) + + model = tf.keras.Model(inputs=inp, outputs=[x]) + super().__init__(model, name) + + def __call__(self, inputs): + # split concatenated inputs + inputs = tf.split(inputs, 2 if self._use_proxy else 3, axis=-1) + + x = self._model(inputs) + return x + diff --git a/phitest/render/image_writer.py b/phitest/render/image_writer.py new file mode 100644 index 0000000..e21f14b --- /dev/null +++ b/phitest/render/image_writer.py @@ -0,0 +1,59 @@ +import numpy as np +import imageio +#import cv2 as cv + +class ImageSequenceWriter(): + def __init__(self): + pass + +try: + import ffmpeg +except ModuleNotFoundError: + pass +else: + class ImageMovieWriter(ImageSequenceWriter): + def __init__(self, file_name, height, width, crf=23, framerate=24): + self.height = height + self.width = width + self.out_stream = (ffmpeg + .input('pipe:', format='rawvideo', pix_fmt='rgb24', s='{}x{}'.format(width, height)) + .output(file_name, framerate=framerate, pix_fmt='yuv420p', crf=crf)#, vcodec='libx264' + .overwrite_output() + .run_async(pipe_stdin=True) + ) + + def imwrite(self, image): + if not (isinstance(image, np.ndarray)): + raise TypeError("image must be a np.ndarray, is: {}".format(image.__class__.__name__)) + if not (len(image.shape)==3): + raise TypeError("image must have shape HWC, is: {}".format(image.shape)) + if not (image.shape[0]==self.height and image.shape[1]==self.width): + raise ValueError("image shape is {}, expected shape is {}".format(image.shape, (self.height, self.width, None))) + + if image.shape[2]==1: + image = np.repeat(image, 3, axis=-1) + elif image.shape[2]==2: + image = np.pad(image, ((0,0),(0,0),(0,1))) + elif image.shape[2]>3: + raise ValueError("input image must have at most 3 channel") + + if not (image.dtype==np.uint8): + image = (image*225.).astype(np.uint8) + + image = np.array(image) + + self.out_stream.stdin.write(image.tobytes()) + + def mimwrite(self, images): + for image in images: + self.imwrite(image) + + def close(self): + self.out_stream.stdin.close() + self.out_stream.wait() + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, exc_traceback): + self.close() \ No newline at end of file diff --git a/phitest/render/lighting.py b/phitest/render/lighting.py new file mode 100644 index 0000000..62d8f95 --- /dev/null +++ b/phitest/render/lighting.py @@ -0,0 +1,194 @@ +import numpy as np +import tensorflow as tf +import logging + +from .camera import Camera +from .transform import Transform, GridTransform +from .serialization import to_dict, from_dict +from lib.tf_ops import shape_list, tf_to_dict + +OLD_LIGHTING = True + +class Light(object): + def __init__(self, color=[1.,1.,1.], intensity=1., reflection=1.): + self.c = np.asarray(color) + self.i = intensity + self.r = reflection # portion of the not transmitted light that is reflected + + @classmethod + def from_dict(cls, d): + return cls(**d) + + @property + def monochrome(self): + return np.isscalar(self.c) or np.asarray(self.c).shape==(1,) + + def grid_lighting(self, denisty_data, density_transforms, renderer=None, scattering_func=None): + # if scattering_func is not None: + # return scattering_func(density=density_transform.data, light_in=self.c * self.i, light=self) + if OLD_LIGHTING: + cell_reflection = denisty_data + else: + cell_transmission = tf.exp(-denisty_data) + cell_reflection = (1.0 - cell_transmission) * self.r + return cell_reflection * self.c * self.i # * setup.rendering.lighting.scattering_ratio + + def to_dict(self): + return { + "intensity":tf_to_dict(self.i), + "color":list(self.c), + } + +class PointLight(Light): + def __init__(self, transform, color=[1.,1.,1.], intensity=1., range_scale=1, range_limit=0): + super().__init__(color, intensity) + self.t = transform + self.r = range_limit + self.s = range_scale + + @classmethod + def from_dict(cls, d): + t = d.pop("transform") + t = from_dict(t) + return cls(t, **d) + + + def grid_lighting_old(self, density_transform, renderer=None, scattering_func=None): + raise NotImplementedError("This is extremely broken!") + #M_M_inv = np.linalg.inv(density_transform.get_transform_matrix()) + # position of light source in grid OS, normalized by size -> [0,1] + # # !problem: pos is xyz, size is zyx + position_grid_OS = (density_transform.get_inverse_transform()@self.t.position_global())[:3]/np.asarray(density_transform.grid_size) + + #normalized coordinate grid zyx + # !problem: +0.5 offset of cell centers + light_data = tf.meshgrid( #linspace: end is inclusive + tf.linspace(0.,1.,density_transform.grid_size[0]), + tf.linspace(0.,1.,density_transform.grid_size[1]), + tf.linspace(0.,1.,density_transform.grid_size[2]), + indexing='ij') + #offset by light position + light_data = tf.transpose(light_data, (1,2,3,0))- position_grid_OS[::-1]# x,y,z -> z,y,x to match grid + #falloff in normalized grid distance + # !problem: no scaling with actual grid size in WS + light_data = 1/(1+tf.pow(tf.norm(light_data, axis=-1, keepdims=True)*self.s, 2.0)) + light_data = light_data * self.c * self.i #tf.repeat(light_data, 3, axis=-1) + return density_transform.data * tf.cast(light_data, tf.float32) + + def grid_lighting(self, denisty_data, density_transforms, renderer=None, scattering_func=None): + # !problem: does not respect shear? + lights_data = [] + for density_transform in density_transforms: + grid_shape = density_transform.grid_shape + grid_size = grid_shape.xyz.value + cell_size_WS = density_transform.cell_size_world() + grid_size_WS = density_transform.grid_size_world() + light_pos_WS = self.t.position_global() + # position of light source in grid OS + light_pos_grid_OS = (density_transform.get_inverse_transform()@light_pos_WS)[:3] + light_pos_grid_OS_WSscaled = light_pos_grid_OS * cell_size_WS.value + + #sclaled coordinate grid xyz, with +0.5 cell-center offset + light_data = tf.meshgrid( #linspace: end is inclusive + tf.linspace(tf.constant(0.5 * cell_size_WS.x, dtype=tf.float32), tf.constant(grid_size_WS.x - 0.5*cell_size_WS.x, dtype=tf.float32), grid_shape.x), + tf.linspace(tf.constant(0.5 * cell_size_WS.y, dtype=tf.float32), tf.constant(grid_size_WS.y - 0.5*cell_size_WS.y, dtype=tf.float32), grid_shape.y), + tf.linspace(tf.constant(0.5 * cell_size_WS.z, dtype=tf.float32), tf.constant(grid_size_WS.z - 0.5*cell_size_WS.z, dtype=tf.float32), grid_shape.z), + indexing='ij') #CWHD + #offset by light position + light_data = tf.transpose(light_data, (3,2,1,0))- light_pos_grid_OS_WSscaled #DHWC, origin at light position + lights_data.append(light_data) + light_data = tf.stack(lights_data, axis=0) + del lights_data + #falloff in WS grid distance + light_data = 1/(1+tf.pow(tf.norm(light_data, axis=-1, keepdims=True)*self.s, 2.0)) + if OLD_LIGHTING: + light_data = light_data * self.c * self.i #tf.repeat(light_data, 3, axis=-1) + #return denisty_data * tf.cast(light_data, tf.float32) # * setup.rendering.lighting.scattering_ratio + # OR + return light_data * super().grid_lighting(denisty_data, density_transforms, renderer=renderer, scattering_func=scattering_func) + + def to_dict(self): + d = super().to_dict() + d.update({ + "transform":to_dict(self.t), + "range_limit":float(self.r), + "range_scale":float(self.s), + }) + return d + +class SpotLight(PointLight): + def __init__(self, transform, color=[1.,1.,1.], intensity=1., range_scale=1, range_limit=0, angle_deg=45, cast_shadows=False, shadow_resolution=[64,64,64], shadow_clip=[1,10], cone_mask=True, static=None): + super().__init__(transform, color, intensity, range_scale, range_limit) + self.a = angle_deg + self.cast_shadows = cast_shadows + self.shadow_resolution = shadow_resolution + self.shadow_cam = Camera(GridTransform.from_transform(transform, shadow_resolution), shadow_clip, fov=self.a, static=static) + self.cone_mask = cone_mask + + @classmethod + def from_dict(cls, d): + t = d.pop("transform") + t = Transform.from_dict(t) + return cls(t, **d) + + def _get_shadow_mask(self): + shadow_mask = tf.meshgrid(tf.linspace(tf.constant(-1., dtype=tf.float32),1.,self.shadow_resolution[1]), tf.linspace(tf.constant(-1., dtype=tf.float32),1.,self.shadow_resolution[2]), indexing='ij') + shadow_mask = tf.transpose(shadow_mask, (1,2,0)) + shadow_mask = tf.norm(shadow_mask, axis=-1, keepdims=True) + shadow_mask = tf.cast(shadow_mask<=1, tf.float32) + return shadow_mask + + def _render_shadow_map(self, denisty_data, density_transforms, renderer): + if self.cast_shadows: + #print(density_transform.data.get_shape(), tf.reduce_mean(density_transform.data)) + shadow_density = renderer.sample_camera(denisty_data, density_transforms, self.shadow_cam, inverse=False, use_step_channel=[0], squeeze_batch=False)#NVDHWC with V=1 + shadow_density = tf.squeeze(shadow_density, 1) + #print(shadow_density.get_shape(), tf.reduce_mean(shadow_density)) + shadow_density = renderer.blending.reduce_grid_blend(shadow_density, renderer.blend_mode, keep_dims=True) + #shadow_density = renderer._blend_grid(shadow_density, renderer.blend_mode, keep_dims=True) + #shift by one cell into depth to avoid cell-self-shadowing (?) + if renderer.boundary_mode=='BORDER': + shadow_shape = shape_list(shadow_density) + shadow_shape[-4] = 1 + pad = tf.zeros(shadow_shape, dtype=tf.float32) + elif renderer.boundary_mode=='CLAMP': + pad = shadow_density[...,:1,:,:,:] + elif renderer.boundary_mode=='WRAP': + pad = shadow_density[...,-1:,:,:,:] + else: + raise ValueError("Unknow boundary_mode %s"%renderer.boundary_mode) + shadow_density = tf.concat([pad, shadow_density[...,:-1,:,:,:]], axis=-4) + + if renderer.blend_mode=='BEER_LAMBERT': + #shadow_density = tf.math.cumsum(shadow_density, axis=-4, exclusive=True) #+ remove cell shift + transmission = tf.exp(-shadow_density) + elif renderer.blend_mode=='ALPHA': + #shadow_density = tf.math.cumprod(shadow_density, axis=-4, exclusive=True) #? TODO + transmission = (1-tf.clip_by_value(shadow_density, 0, 1)) + else: + raise ValueError('Unknown blend_mode \'{}\''.format(renderer.blend_mode)) + else: + transmission = tf.ones([1] + list(self.shadow_resolution) + [1], dtype=tf.float32) + if self.cone_mask: + transmission*=self._get_shadow_mask() + + transmission = tf.squeeze(renderer.sample_camera(transmission, density_transforms, self.shadow_cam, inverse=True, use_step_channel=None, squeeze_batch=False), 1) + #print(transmission.get_shape(), tf.reduce_mean(transmission)) + return transmission + + + def grid_lighting(self, denisty_data, density_transforms, renderer, scattering_func=None): + light = self._render_shadow_map(denisty_data, density_transforms, renderer) + light *= super().grid_lighting(denisty_data, density_transforms, renderer=renderer, scattering_func=scattering_func) + return light + + def to_dict(self): + d = super().to_dict() + d.update({ + "angle_deg":float(self.a), + "cast_shadows":bool(self.cast_shadows), + "shadow_resolution":list(self.shadow_resolution), + "shadow_clip":list(self.shadow_cam.clip), + "cone_mask":bool(self.cone_mask), + }) + return d \ No newline at end of file diff --git a/phitest/render/neural_data_structures.py b/phitest/render/neural_data_structures.py new file mode 100644 index 0000000..bf649ca --- /dev/null +++ b/phitest/render/neural_data_structures.py @@ -0,0 +1,3730 @@ + +import logging, copy, warnings #, itertools # +from numbers import Number, Integral + +import numpy as np +import tensorflow as tf +from .data_structures import DensityGrid, VelocityGrid, State, ResourceCacheDictTF, Sequence +from .vector import GridShape, Vector +from .generator_models import GrowingUNet, DeferredBackpropNetwork +from lib.tf_ops import shape_list, has_shape, has_rank, grad_log, tf_pad_to_next_div_by, tf_norm2 +from lib.data import make_sphere_SDF, make_cube_SDF +from .profiling import SAMPLE + +LOG = logging.getLogger("NeuralStructs") + +def is_None_or_type(value, types): + return (value is None) or isinstance(value, types) + +def _handle_wrong_output_users(output_users, expected_types, name): + users_type_names = {out_id: [type(_).__name__ for _ in users] for out_id, users in output_users} if output_users is not None else None + type_names = {out_id: [_.__name__ for _ in types] for out_id, types in expected_types} + LOG.error("Expected output users of %s to be %s, is %s", name, type_names, users_type_names) + # assert False, "Expected output users of %s to be %s, is %s"%(name, [_.__name__ for _ in expected_types], [type(_).__name__ for _ in output_users] if output_users is not None else None) + +def check_output_users(output_users, expected_types, name): + output_users = output_users or [] + num_output_ids = len(output_users) + exp_num_ids = len(expected_types) + if not (num_output_ids==exp_num_ids and set(output_users.keys())==set(expected_types.keys())): + _handle_wrong_output_users(output_users, expected_types, name) + return + + for out_id, users in output_users.items(): + types = expected_types[out_id] + num_output_users = len(users) + exp_num_users = len(types) + if not num_output_users==exp_num_users: + _handle_wrong_output_users(output_users, expected_types, name) + return + + output_users_types = [type(_) for _ in users] + for t in set(types): + if not output_users_types.count(t)==types.count(t): + _handle_wrong_output_users(output_users, expected_types, name) + return + + #LOG.info("Output users of %s are correct: expected %s, is %s", name, [_.__name__ for _ in expected_types], [type(_).__name__ for _ in output_users] if output_users is not None else None) + +def input_key(other, output_id): + return (other, output_id) + +from abc import ABC, abstractmethod + +class BackpropInterface(ABC): + @abstractmethod + def outputs(self): + raise NotImplementedError + + @abstractmethod + def output(self, output_id): + raise NotImplementedError + + @abstractmethod + def _register_output_user(self, other, output_id): + raise NotImplementedError + + @abstractmethod + def _compute_input_grads(self): + raise NotImplementedError + + @abstractmethod + def _get_input_grad(self, other, output_id): + raise NotImplementedError + + @abstractmethod + def has_gradients_for(self, other, output_id): + raise NotImplementedError + + @abstractmethod + def can_backprop(self): + raise NotImplementedError + + @abstractmethod + def requires_backprop(self): + raise NotImplementedError + +class NeuralGrid(BackpropInterface): + def __init__(self, inputs, models, device=None): + assert isinstance(inputs, (list, tuple)) + assert all(isinstance(inp, BackpropInterface) or callable(inp) for inp in inputs) + assert isinstance(models, (list, tuple)) + assert all(isinstance(m, DeferredBackpropNetwork) for m in models) + + self._inputs = copy.copy(inputs) + self._models = copy.copy(models) + + self.__output_users = {"OUTPUT":[]} + self.__cache = ResourceCacheDictTF(device) + self.__input_ref_cache = None + + def __register_inputs(self): + for inp, out_id in self._inputs: + if isinstance(inp, BackpropInterface): + inp._register_output_user(self, out_id) + + def _register_output_user(self, other, output_id): + if isinstance(other, BackpropInterface) and other not in self.__output_users[output_id]: + self.__output_users[output_id].append(other) + + def __gather_inputs(self): + return [inp(out_id) if callable(inp) else inp.output(out_id) for inp, out_id in self._inputs] + + def _get_input_values(self): + if self.__input_ref_cache is None: + self.__register_inputs() + self.__input_ref_cache = self.__gather_inputs() + return copy.copy(self.__input_ref_cache) + + def _check_inputs(self): + inputs = self.__gather_inputs() + assert self.__input_ref_cache is not None + assert len(self.__input_ref_cache)==len(inputs) + + inputs_shape = [shape_list(_) for _ in inputs] + cache_shape = [shape_list(_) for _ in self.__input_ref_cache] + assert all(a==b for a,b in zip(inputs_shape, cache_shape)), "%s, %s"%(inputs_shape, cache_shape) + assert all(tf.reduce_all(tf.equal(i1, i2)).numpy().tolist() for i1,i2 in zip(self.__input_ref_cache, inputs)) + + #assert all(i1 is i2 for i1,i2 in zip(self.__input_ref_cache, inputs)) + + def _compute_output(self): + raise NotImplementedError + # outp = tf.concat(self._get_input_values(), axis=-1) + # for model in self._models: + # outp = model(outp) + # return outp + + @property + def has_output(self): + return "output" in self.__cache + + def outputs(self): + if "output" not in self.__cache: + # outputs = self._compute_output() + # for out_id, output in outputs.items(): + # self.__cache["output:"+out_id] = output + self.__cache["output"] = self._compute_output()["OUTPUT"] + else: + self._check_inputs() + return {"OUTPUT": self.__cache["output"]} + + def output(self, output_id): + return self.outputs()[output_id] + + def input_index(self, other, output_id): + t = input_key(other, output_id) + if t in self._inputs: + return self._inputs.index(t) + else: + raise KeyError + + def _get_input_grad_scales(self): + return {} + + def _compute_input_grads(self): + if all("input_grads_%d"%(i,) not in self.__cache for i in range(len(self._inputs))): + input_grads = self.__backprop() + scales = self._get_input_grad_scales() + for i, grad in enumerate(input_grads): + if i in scales: + grad = grad * scales[i] + self.__cache["input_grads_%d"%(i,)] = grad + else: + # check if the input is still valid + self._check_inputs() + + def _get_input_grad(self, other, output_id): + # get gradients for a specific input + # check if it is one of the inputs + #assert other in self._inputs + idx = self.input_index(other, output_id) + + self._compute_input_grads() + return self.__cache["input_grads_%d"%(idx,)] + + def has_gradients_for(self, other, output_id): + return (input_key(other, output_id) in self._inputs) and (("input_grads_%d"%(self.input_index(other, output_id),) in self.__cache) or self.can_backprop) + + @property + def can_backprop(self): + # has output gradients available + #LOG.info("%s can backprop: out grad %s; output users %d, provides grads %s", type(self).__name__, "output_grad" in self.__cache, len(self.__output_users), [_.has_gradients_for(self) for _ in self.__output_users]) + return ("output_grad" in self.__cache) or (len(self.__output_users)>0 and any(len(users)>0 and any(_.has_gradients_for(self, out_id) for _ in users) for out_id, users in self.__output_users.items())) + + @property + def requires_backprop(self): + return len(self._models)>0 and any(not _.is_frozen_weights for _ in self._models) and any("input_grads_%d"%(i,) in self.__cache for i in range(len(self._inputs))) and self.can_backprop + + def _has_output_shape(self, tensor): + assert self.has_output + return has_shape(tensor, shape_list(self.output("OUTPUT"))) + + def add_output_grad(self, grad): + assert self._has_output_shape(grad) + if "output_grad" not in self.__cache: + self.__cache["output_grad"] = grad + else: + self.__cache["output_grad"] = self.__cache["output_grad"] + grad + + def __gather_output_grads(self): + + if self.parent_state.next is not None: + check_output_users(self.__output_users, {"OUTPUT": [WarpedDensityGrid, NeuralVelocityGrid]}, "WarpedDensityGrid") + else: + check_output_users(self.__output_users, {"OUTPUT": []}, "last frame WarpedDensityGrid") + #assert self.parent_state.next is None or (self.__output_users is not None and len(self.__output_users)==2), "Expected velocity output users of WarpedDensityGrid to be one WarpedDensityGrid and one NeuralVelocityGrid, is %s"%([type(_).__name__ for _ in self.__output_users] if self.__output_users is not None else None) + + for out_id, users in self.__output_users.items(): + for other in users: + other._compute_input_grads() + + grads = {} + for out_id, users in self.__output_users.items(): + grads[out_id] = tf.zeros_like(self.output(out_id)) + for other in users: + other_grad = other._get_input_grad(self, out_id) + assert has_shape(other_grad, shape_list(grads[out_id])) + grads[out_id] = grads[out_id] + other_grad + + assert len(self.__output_users)==1 and "OUTPUT" in self.__output_users + if "output_grad" in self.__cache: + assert has_shape(self.__cache["output_grad"], shape_list(grads["OUTPUT"])) + grads["OUTPUT"] = grads["OUTPUT"] + self.__cache["output_grad"] + del self.__cache["output_grad"] + + return grads + + def get_variables(self): + return [model.trainable_variables for model in self._models] + + def get_output_variables(self): + return {"output": self.output("OUTPUT")} + + def __backprop(self): + # should only be called once per iteration + assert "output" in self.__cache + self._check_inputs() + + output_grads = self.__gather_output_grads() + + LOG.debug("Backprop %s", type(self).__name__) + variables = {"inputs": self._get_input_values(), "model_params": self.get_variables()} + + with tf.GradientTape(watch_accessed_variables=False) as tape: + tape.watch(variables) + output = self._compute_output() + + gradients = tape.gradient(output, variables, output_gradients=output_grads) + for model, model_grads in zip(self._models, gradients["model_params"]): + model.add_gradients(model_grads) + + return gradients["inputs"] + + def clear_cache(self): + self.__cache.clear() + self.__output_users = {"OUTPUT":[]} + self.__input_ref_cache = None + +class WarpedDensityGrid(DensityGrid, NeuralGrid): + def __init__(self, order=1, dt=1.0, clamp="NONE", device=None, scale_renderer=None, var_name="denstiy", is_SDF=False): + #self.__density = density_input + #self.__velocity = velocity_input + + self.__input_index_density = 0 + self.__input_index_velocity = 1 + NeuralGrid.__init__(self, inputs=[], models=[], device=device) + # self.__order = order + # self.__dt = dt + # self.__clamp = clamp + self.set_warp_params(order, dt, clamp) + + # init DensityGrid + #super().__init__(self, ) + self.is_var = False + self._device = device + self._name = var_name + self._is_trainable = False + self._is_SDF = is_SDF + + self.scale_renderer = scale_renderer + self.hull = None + self.restrict_to_hull = False + + self._inflow = None + + self.set_density_grad_scale(1) + + @property + def shape(self): + try: + shape = self.__density_grid_input.shape + except IndexError: # inputs not yet set + shape = self.parent_state.prev.density.shape + return shape + + @property + def _d(self): + return self.output("OUTPUT") + + @property + def d(self): + # the density grid with modifications (inflow) and constraints (hull, non-negativity) + d = self._d + if not self._is_SDF: + d = tf.maximum(d, 0) + return d + + def set_warp_params(self, order, dt=1.0, clamp="NONE"): + LOG.info("Set WarpedDensityGrid warp: order=%d, dt=%f, clamp=%s", order, dt, clamp) + self.__order = order + self.__dt = dt + self.__clamp = clamp + + def __set_inputs(self): + assert hasattr(self, "parent_state") and self.parent_state is not None and self.parent_state.prev is not None + ps = self.parent_state.prev + density_input = ps.density + velocity_input = ps.velocity + assert isinstance(density_input, (DensityGrid)) and isinstance(density_input, BackpropInterface) + assert isinstance(velocity_input, (VelocityGrid)) and isinstance(velocity_input, BackpropInterface) + self._inputs = [(density_input, "OUTPUT"), (velocity_input, "CENTERED")] + + def set_density_grad_scale(self, value): + self.__density_grad_scale = tf.constant(value, dtype=tf.float32) + + def _get_input_grad_scales(self): + return {self.__input_index_density: self.__density_grad_scale} + + @property + def __density_grid_input(self): + return self._inputs[self.__input_index_density][0] + @property + def __velocity_grid_input(self): + return self._inputs[self.__input_index_velocity][0] + + def _compute_output(self): + self.__set_inputs() + with SAMPLE("Warp Denisty Grid"): + with SAMPLE("get inputs"): + inputs = self._get_input_values() + density = inputs[self.__input_index_density] + velocity = inputs[self.__input_index_velocity] + velocity_grid = self.__velocity_grid_input + dens_shape = shape_list(density)[1:-1] + if not dens_shape == shape_list(velocity)[1:-1]: + with SAMPLE("scale velocity"): + velocity = NeuralVelocityGrid.resample_velocity(velocity_grid.scale_renderer, velocity, shape=dens_shape, is_staggered=False, scale_magnitude=True) + with SAMPLE("warp"): + density_warped = velocity_grid.warp(density, centered_velocity=velocity, order=self.__order, dt=self.__dt, clamp=self.__clamp) + return {"OUTPUT": density_warped} + + # inherited from DensityGrid + # def get_output_variables(self): + # return {"output": self.output()} + + def set_output_gradients_for_backprop_accumulate(self, output_gradients, **kwargs): + self.add_output_grad(output_gradients["density"]) + + def apply_clamp(self, vmin, vmax): + pass + + def assign(self, d, inflow=None): + raise TypeError("Can't assign to WarpedDensityGrid") + + def clear_cache(self): + self._inputs = [] + super().clear_cache() + NeuralGrid.clear_cache(self) + +class NeuralDensityGrid(DensityGrid, BackpropInterface): + def __init__(self, volume_decoder, parent_state, scale_renderer=None, hull=None, inflow=None, inflow_offset=None, inflow_mask=None, device=None, var_name="denstiy", trainable=True, restrict_to_hull=True, \ + step_input_density=[], step_input_density_target=[], step_input_features=[0,1], type_input_features=["TARGET_UNPROJECTION"], base_input="ZERO", is_SDF=False, base_SDF_mode="NONE"): + self._device = device + self.cache_output = True + self.__input_grad_cache = ResourceCacheDictTF(self._device) + self.clear_cache() + self.parent_state = parent_state + self.use_raw_images = True + self.volume_decoder = volume_decoder + if hull is not None: + raise NotImplementedError("NeuralDensityGrid does not support hull") + self.hull = None + self.restrict_to_hull = restrict_to_hull + if inflow is not None: + raise NotImplementedError("NeuralDensityGrid does not support inflow") + self._inflow = None + self.scale_renderer = scale_renderer + + # needed for copy to fixed DensityGrid + self.is_var = False + self._name = var_name + self._is_trainable = trainable + self._is_SDF = is_SDF + #raise NotImplementedError() + + self.recursive_MS = False + + assert isinstance(step_input_density, (list, tuple, np.ndarray)) and all(isinstance(_, Integral) for _ in step_input_density) + if 0 in step_input_density: raise ValueError("Can't use own output as input") + self.step_input_density = step_input_density + assert isinstance(step_input_density_target, (list, tuple, np.ndarray)) and all(isinstance(_, Integral) for _ in step_input_density_target) + self.step_input_density_target = step_input_density_target + assert isinstance(step_input_features, (list, tuple, np.ndarray)) and all(isinstance(_, Integral) for _ in step_input_features) + self.step_input_features = step_input_features + + # also check requires_parent_state_variables if adding new feature inputs + assert isinstance(type_input_features, (list, tuple, np.ndarray)) and all(isinstance(_, str) for _ in type_input_features) + assert all(_ in ["INPUT_IMAGES_UNPROJECTION","INPUT_IMAGES_RAW_UNPROJECTION","INPUT_IMAGES_HULL","ENC3D"] for _ in type_input_features) + # DEBUG + assert len(type_input_features)==1 and type_input_features[0]=="ENC3D" + self.type_input_features = type_input_features + + assert base_input in ["ZERO", "INPUT_IMAGES_HULL"] #TARGET_HULL + self.base_input_features = base_input #previous density input of the lowest resolution + + self.norm_input_mode = "NONE" # debug, was GROUP. NONE, SINGLE, GROUP, ALL + + LOG.info("Setup NeuralDensityGrid: SDF=%s, use raw=%s, step_input_density=%s, step_input_density_target=%s, step_input_features=%s, type_input_features=%s, base_input_features=%s, norm_input_mode=%s", self._is_SDF, self.use_raw_images, self.step_input_density, self.step_input_density_target, self.step_input_features, self.type_input_features, self.base_input_features, self.norm_input_mode) + + + assert base_SDF_mode in ["NONE", "RESIDUAL", "INPUT_RESIDUAL"] + self.__use_base_SDF = (base_SDF_mode in ["RESIDUAL", "INPUT_RESIDUAL"]) and self._is_SDF + self.__input_base_SDF = (base_SDF_mode=="INPUT_RESIDUAL") and self.__use_base_SDF + self.__base_SDF = None + self.__base_SDF_strength = 4 #number of SDF border cells + + # backprop interface + def __add_used_input(self, other, output_id): + self.__inputs_for_backprop.append(input_key(other, output_id)) + + def __register_inputs(self): + for inp, out_id in self.__inputs_for_backprop: + assert isinstance(inp, BackpropInterface) + inp._register_output_user(self, out_id) + + def __gather_inputs(self): + return [inp.output(out_id) for inp, out_id in self.__inputs_for_backprop] + + def outputs(self): + return {"OUTPUT": self._d} + + def output(self, output_id): + return self.outputs()["OUTPUT"] + + def _register_output_user(self, other, output_id): + assert isinstance(other, BackpropInterface) + if other not in self.__output_users[output_id]: + self.__output_users[output_id].append(other) + + def _compute_input_grads(self): + if self.__input_grad_cache.is_empty(): + assert self.__output_gradient_cache is not None + self.backprop_accumulate(**self.__output_gradient_cache) + self.__output_gradient_cache = None + + def _get_input_grad(self, other, output_id): + assert not self.__input_grad_cache.is_empty() + t = input_key(other, output_id) + idx = self.__inputs_for_backprop.index(t) + return self.__input_grad_cache["input_grad_%d"%(idx,)] + + def has_gradients_for(self, other, output_id): + return (input_key(other, output_id) in self.__inputs_for_backprop) and (not self.__input_grad_cache.is_empty() or self.can_backprop) + + @property + def can_backprop(self): + # has output gradients available + return (self.__output_gradient_cache is not None) or (len(self.__output_users)>0 and any(len(users)>0 and any(_.has_gradients_for(self, out_id) for _ in users) for out_id, users in self.__output_users.items())) + + @property + def requires_backprop(self): + return (not self.volume_decoder.is_frozen_weights) and self.__input_grad_cache.is_empty() and self.can_backprop + + # own methods + + @property + def shape(self): + if self.__state is None: raise ValueError("Parent state is not set") + return self.__state.transform.grid_size + @property + def parent_state(self): + return self.__state + @parent_state.setter + def parent_state(self, value): + assert is_None_or_type(value, NeuralState) + self.__state = value + + def _get_base_SDF(self): + if self.__base_SDF is None or shape_list(self.__base_SDF)[1:-1]!= self.shape: + shape = np.asarray(self.shape, dtype=np.float32) + self.__base_SDF = tf.maximum(1, make_cube_SDF(self.shape, np.maximum(0, shape/2 - self.__base_SDF_strength))) + return self.__base_SDF + + def _get_batch_size(self): + if self.__d is not None: + return shape_list(self.__d)[0] + elif self.parent_state is not None: + return self.parent_state._get_batch_size() + else: + raise RuntimeError("NeuralVelocityGrid: Can't determine batch size without cached velocity or parent state.") + #return self._batch_size + + def rescale(self, new_shape): + raise NotImplementedError + self.parent_state.transform.grid_size = new_shape + + def _get_generator_input(self): + if "BASE" not in self.__generator_inputs: + shape = self.shape #self.centered_shape + with SAMPLE("density base input"): + inp = [] + for step in self.step_input_density: + raise RuntimeError("DEBUG") + state = self._get_state_by_step(step) + if state is not None: + if state.density is self: raise RuntimeError("Can't use own output as input") + inp.append(state.density.scaled(shape, with_inflow=True)) + #inp.append(tf.zeros_like(state.density.scaled(self.centered_shape, with_inflow=True))) + feature_shape = shape_list(inp[-1]) + else: + LOG.debug("NeuralDensityGrid of frame %d is missing density input of state %d (step %d)", self.parent_state.frame, self.parent_state.frame+step, step) + inp.append(tf.zeros(feature_shape)) + + for step in self.step_input_density_target: + raise RuntimeError("DEBUG") + state = self._get_state_by_step(step) + if state is not None: + inp.append(state.density_target.scaled(shape, with_inflow=True)) + #inp.append(tf.zeros_like(state.density.scaled(self.centered_shape, with_inflow=True))) + feature_shape = shape_list(inp[-1]) + else: + LOG.debug("NeuralDensityGrid of frame %d is missing density target input of state %d (step %d)", self.parent_state.frame, self.parent_state.frame+step, step) + inp.append(tf.zeros(feature_shape)) + + for step in self.step_input_features: + state = self._get_state_by_step(step) + if state is not None: + assert self.type_input_features==["ENC3D"] + inp.append(state.output("OUTPUT")) + assert has_shape(inp[-1], [None]+shape+[None]), "%s, %s"%(shape_list(inp[-1]), [None]+shape+[None]) + #self.__inputs_for_backprop.append(state) + self.__add_used_input(state, "OUTPUT") + #assert feature_shape[-1] == 78, "debug: %s"%(feature_shape,) + else: + LOG.debug("NeuralDensityGrid of frame %d is missing feature input of state %d (step %d)", self.parent_state.frame, self.parent_state.frame+step, step) + inp.append(tf.zeros(feature_shape)) + + self.__register_inputs() + + self.__generator_inputs["BASE"] = inp + + return self.__generator_inputs["BASE"] + + + def _get_state_by_step(self, step): + assert isinstance(step, Integral) and step>=0 + state = self.parent_state + while step>0: + if state.next is None: + #raise AttributeError("Can't get next state") + return None + state = state.next + step -=1 + return state + + def _get_generator_input_MS(self, scale): + #raise NotImplementedError("TODO") + recurrent_input_dens = False + warp_recurrent_dens = False #try this? + # TODO: recursive_MS input generation; possibly multi-scale input/features provided by parent state + if scale not in self.__generator_inputs: + if self._is_top_scale(scale) or self._parent_provides_input_scale(scale): + scale_shape = self.shape_of_scale(scale) #self.centered_shape + with SAMPLE("density top MS input"): + inp = [] + for step in self.step_input_density: + raise RuntimeError("DEBUG") + state = self._get_state_by_step(step) + if state is not None: + if state.density is self: raise RuntimeError("Can't use own output as input") + inp.append(state.density.scaled(scale_shape, with_inflow=True)) + #inp.append(tf.zeros_like(state.density.scaled(self.centered_shape, with_inflow=True))) + feature_shape = shape_list(inp[-1]) + else: + LOG.debug("NeuralDensityGrid of frame %d is missing density input of state %d (step %d)", self.parent_state.frame, self.parent_state.frame+step, step) + inp.append(tf.zeros(feature_shape)) + + for step in self.step_input_density_target: + raise RuntimeError("DEBUG") + state = self._get_state_by_step(step) + if state is not None: + inp.append(state.density_target.scaled(scale_shape, with_inflow=True)) + #inp.append(tf.zeros_like(state.density.scaled(self.centered_shape, with_inflow=True))) + feature_shape = shape_list(inp[-1]) + else: + LOG.debug("NeuralDensityGrid of frame %d is missing density target input of state %d (step %d)", self.parent_state.frame, self.parent_state.frame+step, step) + inp.append(tf.zeros(feature_shape)) + + for step in self.step_input_features: + state = self._get_state_by_step(step) + if state is not None: + #if self.use_raw_images: + # inp.append(state.get_volume_features(types=self.type_input_features, shape=scale_shape)) #shape=scale_shape + # feature_shape = shape_list(inp[-1]) + # raise NotImplementedError("TODO:") + assert self.type_input_features==["ENC3D"] + inp.append(state.output("OUTPUT")) + assert has_shape(inp[-1], [None]+scale_shape+[None]), "%s, %s"%(shape_list(inp[-1]), [None]+scale_shape+[None]) + #self.__inputs_for_backprop.append(state) + self.__add_used_input(state, "OUTPUT") + #assert feature_shape[-1] == 78, "debug: %s"%(feature_shape,) + else: + LOG.debug("NeuralDensityGrid of frame %d is missing feature input of state %d (step %d)", self.parent_state.frame, self.parent_state.frame+step, step) + inp.append(tf.zeros(feature_shape)) + + + if recurrent_input_dens and not self.parent_state.prev.density.has_MS_output: + if self.parent_state.prev is not None: + inp.append(self.parent_state.prev.density.d_MS(scale)) + + self.__register_inputs() + + #if self.is_staggered: + # inp = [self._scalar_centered_to_staggered(_) for _ in inp] + else: + inp = self._get_generator_input_MS(self._get_larger_scale(scale)) + inp = [self._scale_input_down(_, scale) for _ in inp] + + if recurrent_input_dens: + if self.parent_state.prev is None: + inp.append(tf.zeros(self.shape_of_scale(scale))) + elif self.parent_state.prev.density.has_MS_output: + inp.append(self.parent_state.prev.density.d_MS(scale)) + else: + raise RuntimeError + + + + + self.__generator_inputs[scale] = inp + + return self.__generator_inputs[scale] + + def _get_top_scale(self): + return self.recursive_MS_scales[self.recursive_MS_max_level] + def _get_top_active_scale(self): + return self.recursive_MS_scales[self.recursive_MS_current_level] + def _is_top_scale(self, scale): + return scale==self.recursive_MS_scales[self.recursive_MS_current_level] + def _parent_provides_input_scale(self, scale): + return self.recursive_MS_direct_input + #return False + def _get_larger_scale(self, scale): + return self.recursive_MS_scales[self.recursive_MS_scales.index(scale)+1] + + + def set_recursive_MS(self, num_scales: Integral, scale_factor: Number, shared_decoder=True, train_mode="ALL", shapes=None, shape_cast_fn=round, as_residual=True, direct_input=False): + self.recursive_MS = True + self.recursive_MS_direct_input = direct_input + self.recursive_MS_residual = as_residual #True + self.recursive_MS_max_level = num_scales-1 + self.recursive_MS_current_level = self.recursive_MS_max_level + assert isinstance(scale_factor, Number) + if scale_factor<=0: LOG.warning("recursive MS scale factor should be positive. is: %s", scale_factor) + self.recursive_MS_scale_factor = scale_factor + self.recursive_MS_scale_magnitude = True + self.recursive_MS_scales = list(range(num_scales)) + self.recursive_MS_shape_cast_fn = shape_cast_fn + if shapes is None: + self._recursive_MS_shapes = list(reversed([[int(shape_cast_fn(_/(self.recursive_MS_scale_factor**scale))) for _ in self.shape] for scale in self.recursive_MS_scales])) + else: + assert isinstance(shapes, list) and len(shapes)==num_scales + assert all(isinstance(shape, list) for shape in shapes) + assert all(has_shape(_, shape_list(self.shape)) for _ in shapes) + if not all(shapes[-1]==self.shape): LOG.warning("Maximum resolution of shapes provided for recursive MS {} does not match main resolution of this NeuralDensityGrid {}".format(shapes[-1], self.shape)) + if not all(all(shapes[i]<=shapes[+1]) for i in range(len(shapes)-1)): LOG.warning("Shapes provided for recursive MS are not growing in all dimensions: {}".format(shapes)) + self._recursive_MS_shapes = copy.deepcopy(shapes) + #self.set_recursive_MS_level(num_scales, scale_factor, shapes, shape_cast_fn) + + self.recursive_MS_input_dens = True + if shared_decoder: + assert isinstance(self.volume_decoder, (DeferredBackpropNetwork, tf.keras.Model)) + else: + assert isinstance(self.volume_decoder, list) \ + and len(self.volume_decoder)==(self.recursive_MS_max_level+1) \ + and all(isinstance(_, (DeferredBackpropNetwork, tf.keras.Model)) for _ in self.volume_decoder) + self.recursive_MS_shared_decoder = shared_decoder + assert train_mode in ["ALL", "TOP"] + self.recursive_MS_train_mode = train_mode #"ALL" if self.recursive_MS_shared_decoder else "TOP" #ALL, TOP; no effect with shared decoder + self.cache_MS = True + + LOG.info("Setup %srecursive multi-scale for NeuralDensityGrid of frame %s with %d scales (%s), factor %f, %s decoders, training '%s'. The output scales are%s cached.", \ + "residual " if self.recursive_MS_residual else "", \ + "?" if self.parent_state is None else self.parent_state.frame, self.num_recursive_MS_scales, self.recursive_MS_shapes, self.recursive_MS_scale_factor, \ + "shared" if self.recursive_MS_shared_decoder else "separate", self.recursive_MS_train_mode, "" if self.has_MS_output else " not") + + if self.recursive_MS_shared_decoder and self.recursive_MS_train_mode!="ALL": + LOG.warning("recursive_MS_train_mode 'TOP' has no effect on shared decoder.") + + @property + def recursive_MS_shapes(self): + if self.recursive_MS_shared_decoder: + return list(reversed([[int(self.recursive_MS_shape_cast_fn(_/(self.recursive_MS_scale_factor**scale))) for _ in self.shape] for scale in self.gen_current_MS_scales()])) + else: + return self._recursive_MS_shapes + + def set_recursive_MS_level(self, level: Integral, copy_weights: bool = False): + if not self.recursive_MS: raise RuntimeError("recursive_MS was not set up.") + if level<0 or self.recursive_MS_max_level0: + inputs_frames.insert(0, normalize_tensor(tf.concat(dens_inputs, axis=-1))) + del dens_inputs + + if num_feature_inputs>0: + inputs_frames.insert(1, normalize_tensor(tf.concat(features_inputs, axis=-1))) + del features_inputs + + elif self.norm_input_mode=="ALL" and (num_density_inputs+num_feature_inputs)>0: + inputs_frames = [normalize_tensor(tf.concat(inputs_frames[:num_density_inputs+num_feature_inputs], axis=-1))] + inputs_frames[num_density_inputs+num_feature_inputs:] + + # dens is also input + if self.recursive_MS_input_dens: + # LOG.warning("TEST: NO RECURSIVE DENSITY INPUT!") + # inputs_frames.append(tf.zeros_like(dens)) + inputs_frames.append(dens) + + #LOG.info("input stats: %s", [(tf.reduce_mean(_).numpy().tolist(), tf.reduce_min(_).numpy().tolist(), tf.reduce_max(_).numpy().tolist()) for _ in inputs_frames]) + inputs_frames = tf.concat(inputs_frames, axis=-1) + + with SAMPLE("volume_decoder"): + if self.recursive_MS_shared_decoder: + dens_residual = self.volume_decoder(inputs_frames) + else: + dens_residual = self.volume_decoder[s](inputs_frames) + + if False: + warnings.warn("Sigmoid on density residual output.") + dens_residual = tf.math.sigmoid(dens_residual) * 4 - 2 + + if self.recursive_MS_residual: + #LOG.warning("TEST: NO DENSITY GENERATOR!") + #dens = dens + 0.0*dens_residual + #dens = (dens + dens_residual)*0.5 + dens = dens + dens_residual + else: + dens = dens_residual + + if self.cache_MS: + with tf.device(self._device): + self.__d_MS[s] = tf.identity(dens) + self.__d_MS_residual[s] = tf.identity(dens_residual) + + + self.clear_input_cache() #? + d = dens + else: + inputs_frames = self._get_generator_input() #list of inputs for current level/scale + + num_density_inputs = len(self.step_input_density) + len(self.step_input_density_target) + num_feature_inputs = len(self.step_input_features) + if self.norm_input_mode=="SINGLE": + #inputs_frames = map(normalize_tensor, inputs_frames) + for idx in range(num_density_inputs+num_feature_inputs): + # we don't want to normalize velocities + inputs_frames[idx] = normalize_tensor(inputs_frames[idx]) + + elif self.norm_input_mode=="GROUP": + #inputs_frames at this point: densities, features, recurrent velocity + dens_inputs = inputs_frames[:num_density_inputs] + features_inputs = inputs_frames[num_density_inputs:num_density_inputs+num_feature_inputs] + inputs_frames = inputs_frames[num_density_inputs+num_feature_inputs:] + + if num_density_inputs>0: + inputs_frames.insert(0, normalize_tensor(tf.concat(dens_inputs, axis=-1))) + del dens_inputs + + if num_feature_inputs>0: + inputs_frames.insert(1, normalize_tensor(tf.concat(features_inputs, axis=-1))) + del features_inputs + + elif self.norm_input_mode=="ALL" and (num_density_inputs+num_feature_inputs)>0: + inputs_frames = [normalize_tensor(tf.concat(inputs_frames[:num_density_inputs+num_feature_inputs], axis=-1))] + inputs_frames[num_density_inputs+num_feature_inputs:] + + if self.__input_base_SDF: + inputs_frames.append(tf.tile(self._get_base_SDF(), [self._get_batch_size(),1,1,1,1])) + + #LOG.info("input stats: %s", [(tf.reduce_mean(_).numpy().tolist(), tf.reduce_min(_).numpy().tolist(), tf.reduce_max(_).numpy().tolist()) for _ in inputs_frames]) + inputs_frames = tf.concat(inputs_frames, axis=-1) + + with SAMPLE("volume_decoder"): + dens = self.volume_decoder(inputs_frames) + + if False: + warnings.warn("Sigmoid on density residual output.") + dens = tf.math.sigmoid(dens) * 4 - 2 + + self.clear_input_cache() #? + d = dens + + if self.__use_base_SDF: + d = d + self._get_base_SDF() + return d + + @property + def _d(self): + # the raw density grid + if self.__d is not None: + d = self.__d + else: + d = self.generate_denstiy() + if self.cache_output: + with tf.device(self._device): + self.__d = tf.identity(d) + d = self.__d + return d + @_d.setter + def _d(self, value): + raise AttributeError("Can't set density of NeuralDensityGrid") + + @property + def d(self): + # the density grid with modifications (inflow) and constraints (hull, non-negativity) + d = self._d + if not self._is_SDF: + d = tf.maximum(d, 0) + return d + + def _d_MS(self, scale): + if not self.has_MS_output: raise ValueError("Mutiscale density is not available.") + # check if base vel has been generated. if not, do so to also generate the MS scales. + if self.__d is not None: + if scale not in self.__d_MS: #density has been generated, but the scale is (still) not available + raise ValueError("Mutiscale density of scale \"{}\" is not available. Available scales: {}".format(scale, list(self.__d_MS.keys()))) + else: + if scale not in self.recursive_MS_scales: + raise ValueError("Mutiscale density of scale \"{}\" is not available. Available scales: {}".format(scale, self.recursive_MS_scales)) + self._d #generate density + d = self.__d_MS[scale] + return d + + + def d_MS(self, scale): + d = self._d_MS(scale) + if not self._is_SDF: + d = tf.maximum(d, 0) + return d + + def _d_MS_r(self, scale): + if not self.has_MS_output: raise ValueError("Mutiscale density is not available.") + # check if base vel has been generated. if not, do so to also generate the MS scales. + if self.__d is not None: + if scale not in self.__d_MS_residual: #density has been generated, but the scale is (still) not available + raise ValueError("Mutiscale density of scale \"{}\" is not available. Available scales: {}".format(scale, list(self.__d_MS_residual.keys()))) + else: + if scale not in self.recursive_MS_scales: + raise ValueError("Mutiscale density of scale \"{}\" is not available. Available scales: {}".format(scale, self.recursive_MS_scales)) + self._d #generate density + d = self.__d_MS_residual[scale] + return d + + def d_MS_r(self, scale): + return self._d_MS_r(scale) + + @property + def requires_parent_state_variables(self): + return False #"ENC3D" in self.type_input_features + + def get_variables(self, scale=None): + if self.has_multiple_decoders: #recursive_MS and (not self.recursive_MS_shared_decoder): + if scale is None: + if self.recursive_MS_train_mode=="ALL": + var_dict = {'density_decoder': [var for dec in self.active_decoders for var in dec.trainable_variables]} + elif self.recursive_MS_train_mode=="TOP": + var_dict = {'density_decoder': self.volume_decoder[self.recursive_MS_scales[self.recursive_MS_current_level]].trainable_variables} + else: raise ValueError + else: + var_dict = {'density_decoder': self.volume_decoder[scale].trainable_variables} + else: + var_dict = {'density_decoder': self.volume_decoder.trainable_variables} + + if self.requires_parent_state_variables and self.parent_state is not None: + var_dict.update(self.parent_state.get_variables()) + if self._inflow is not None: + var_dict['inflow'] = self._inflow + return var_dict + + def get_output_variables(self, include_MS=False, include_residual=False, only_trainable=False): + if not self.cache_output: + raise RuntimeError("Output caching must be enabled for output variables to have meaning.") + var_dict = {} + if include_MS and self.has_MS_output: + scales = list(self.gen_current_trainable_MS_scales()) if only_trainable else list(self.gen_current_MS_scales()) + for scale in scales: + var_dict['density_%s'%(scale,)] = self._d_MS(scale) + if include_residual: + var_dict['density_r%s'%(scale,)] = self._d_MS_r(scale) + # else: + # var_dict['density'] = self._d + var_dict['density'] = self._d + + return var_dict + + def __get_MS_variable_keys_scale(self, scale, include_residual=False): + key_list = [] + key_list.append('density_%s'%(scale,)) + if include_residual: + key_list.append('density_r%s'%(scale,)) + + return key_list + + def _get_output_variable_keys(self, include_MS=False, include_residual=False): + key_list = [] + if include_MS: + for scale in self.gen_current_MS_scales(): + key_list.extend(self.__get_MS_variable_keys_scale(scale, include_residual=include_residual)) + # else: + # key_list.append('density') + key_list.append('density') + + return key_list + + def map_gradient_output_to_MS(self, grad_dict): + assert isinstance(grad_dict, dict) + output_keys = self._get_output_variable_keys(include_MS=False) + MS_keys = self.__get_MS_variable_keys_scale(scale=self._get_top_active_scale(), include_residual=False) + assert len(output_keys)==len(MS_keys) + + out_dict = {} + for key, grad in grad_dict.items(): + if key not in output_keys: raise KeyError("Invalid output gradient key '{}'".format(key)) + out_dict[MS_keys[output_keys.index(key)]] = grad + return out_dict + + def apply_clamp(self, vmin, vmax): + pass + + def assign(self, d, inflow=None): + raise TypeError("Can't assign to NeuralDensityGrid") + + def clear_cache(self): + self.__d = None + self.__d_MS = {} + self.__d_MS_residual = {} + self.clear_input_cache() + + self.__output_users = {"OUTPUT": []} + self.__output_gradient_cache = None + self.__inputs_for_backprop = [] + self.__input_grad_cache.clear() + + def clear_input_cache(self): + self.__generator_inputs = {} + + def clear_cache_for_backprop(self): + # self.clear_cache() + # if self.requires_parent_state_variables: + # self.parent_state.clear_cache_for_backprop() + self.__d = None + self.__d_MS = {} + self.__d_MS_residual = {} + self.clear_input_cache() + + def _get_output_users_grads(self): + for out_id, users in self.__output_users.items(): + for other in users: + other._compute_input_grads() + extra_output_gradients = {} + for out_id, users in self.__output_users.items(): + extra_output_gradients[out_id] = tf.zeros_like(self.output(out_id)) + for other in users: + extra_output_gradients[out_id] = extra_output_gradients[out_id] + other._get_input_grad(self, out_id) + return extra_output_gradients + + def backprop(self, output_gradients, include_MS=False, include_residual=False, only_trainable=False): + + if self.parent_state.next is not None: + check_output_users(self.__output_users, {"OUTPUT": [WarpedDensityGrid, NeuralVelocityGrid]}, "NeuralDensityGrid") + else: + check_output_users(self.__output_users, {"OUTPUT": []}, "single frame NeuralDensityGrid") + #assert self.__output_users is not None and len(self.__output_users)==2, "Expected velocity output users of NeuralDensityGrid to be one WarpedDensityGrid and one NeuralVelocityGrid, is %s"%([type(_).__name__ for _ in self.__output_users] if self.__output_users is not None else None) + + extra_output_gradients = self._get_output_users_grads() + + extra_output_gradients = {"density":extra_output_gradients["OUTPUT"]} + if include_MS: + extra_output_gradients = self.map_gradient_output_to_MS(extra_output_gradients) + + for k in output_gradients: + if k in extra_output_gradients: + if output_gradients[k] is None: + output_gradients[k] = extra_output_gradients[k] + else: + output_gradients[k] += extra_output_gradients[k] + + LOG.debug("Backprop density frame %d", self.parent_state.frame) + + with SAMPLE("NDG backprop"): + if not include_MS and not ('density' in output_gradients): + raise ValueError("No gradients to backprop.") + if include_MS: + for scale in (self.gen_current_trainable_MS_scales() if only_trainable else self.gen_current_MS_scales()): + if not ('density_%s'%(scale,) in output_gradients) or (include_residual and not ('density_r%s'%(scale,) in output_gradients)): + raise ValueError("No gradients to backprop for MS scale %s."%(scale,)) + self.clear_cache_for_backprop() + var_dict = self.get_variables() + var_dict["inputs_for_backprop"] = self.__gather_inputs() + + with SAMPLE("forward"), tf.GradientTape(watch_accessed_variables=False) as tape: + tape.watch(var_dict) + output = self.get_output_variables(include_MS=include_MS, include_residual=include_residual) + + output = {k: output[k] for k in output if (k in output_gradients)} + output_gradients = { + k: (output_gradients[k] if output_gradients[k] is not None else tf.zeros_like(output[k])) \ + for k in output_gradients \ + if k in output \ + } + + # for k in output: + # LOG.info("vel output '%s': out type=%s, grad type=%s", k, output[k].__class__.__name__, output_gradients[k].__class__.__name__) + with SAMPLE("gradients"): + gradients = tape.gradient(output, var_dict, output_gradients=output_gradients) + + for i, grad in enumerate(gradients["inputs_for_backprop"]): + self.__input_grad_cache["input_grad_%d"%(i,)] = grad + del gradients["inputs_for_backprop"] + + return gradients + + def __split_gradients_for_decoders(self, grads): + assert self.has_multiple_decoders and self.recursive_MS_train_mode=="ALL" + num_var_per_decoder = [len(dec.trainable_variables) for dec in self.active_decoders] + if sum(num_var_per_decoder)!=len(grads): raise RuntimeError("gradients do not match variables: got {} gradients for {} variables of {} decoders {}".\ + format(len(grads), sum(num_var_per_decoder), len(self.active_decoders), num_var_per_decoder)) + split_grads = [] + pos = 0 + for size in num_var_per_decoder: + split_grads.append(grads[pos:pos+size]) + pos +=size + assert all(size==len(grad) for size, grad in zip(num_var_per_decoder, split_grads)) + return split_grads + + def set_output_gradients_for_backprop_accumulate(self, output_gradients, **kwargs): + kwargs["output_gradients"] = output_gradients + self.__output_gradient_cache = kwargs + + def backprop_accumulate(self, output_gradients, include_MS=False, include_residual=False, only_trainable=False): + gradients = self.backprop(output_gradients, include_MS=include_MS, include_residual=include_residual, only_trainable=only_trainable) + + with SAMPLE("NDG acc grads"): + dens_net_gradients = gradients['density_decoder'] + if self.has_multiple_decoders: + if self.recursive_MS_train_mode=="ALL": + for dec, grads in zip(self.volume_decoder, self.__split_gradients_for_decoders(dens_net_gradients)): + dec.add_gradients(grads) + elif self.recursive_MS_train_mode=="TOP": + self.volume_decoder[self.recursive_MS_scales[self.recursive_MS_current_level]].add_gradients(dens_net_gradients) + else: raise ValueError + else: + self.volume_decoder.add_gradients(dens_net_gradients) + + if self.requires_parent_state_variables: + raise NotImplementedError("Should be handled via BackpropInterface now.") + state_gradients = {k: gradients[k] for k in self.parent_state.get_variables()} + self.parent_state.accumulate_gradients(state_gradients) + + @property + def has_pending_gradients(self): + if self.has_multiple_decoders: + return any(_.has_pending_gradients for _ in self.volume_decoder) + else: + return self.volume_decoder.has_pending_gradients + + def apply_gradients(self, optimizer, keep_gradients=False): + raise NotImplementedError + LOG.debug("Applying density gradients of frame %d", self.parent_state.frame) + if self.has_multiple_decoders: + if not self.has_pending_gradients: raise RuntimeError("No decoder has any recorded gradients to apply."%(self.name,)) + for dec in self.volume_decoder: + if dec.has_pending_gradients: + dec.apply_gradients(optimizer) + if not keep_gradients: + dec.clear_gradients() + else: + self.volume_decoder.apply_gradients(optimizer) + if not keep_gradients: + self.volume_decoder.clear_gradients() + + def get_grads_vars(self, keep_gradients=True): + grads_vars = [] + if self.has_multiple_decoders: + for dec in self.volume_decoder: + if dec.has_pending_gradients: + grads_vars.extend(dec.get_grads_vars(keep_gradients=keep_gradients)) + if not keep_gradients: + dec.clear_gradients() + else: + grads_vars.extend(self.volume_decoder.get_grads_vars(keep_gradients=keep_gradients)) + if not keep_gradients: + self.volume_decoder.clear_gradients() + return grads_vars + + def clear_gradients(self): + if self.has_multiple_decoders: + for dec in self.volume_decoder: + dec.clear_gradients() + else: + self.volume_decoder.clear_gradients() + + + +# generate velocity via network with input from 2 consecutive frames +# input: lifting (density decoder input), generated density +# ouput: centered (or staggered?) velocity +# cache: both centered and staggered velocity, generate one from the other. +# rest of functionality inherited from normal/super VelocityGrid +class NeuralVelocityGrid(VelocityGrid, BackpropInterface): + def __init__(self, volume_decoder, parent_state, boundary=None, scale_renderer=None, warp_renderer=None, device=None, var_name="velocity", trainable=True, velocity_format="CENTERED", \ + step_input_density=[], step_input_density_target=[], step_input_density_proxy=[], step_input_features=[0,1], type_input_features=["TARGET_UNPROJECTION"], warp_input_indices=[0], \ + downscale_input_modes=["RESAMPLE"]): + self.cache_output = True + self._device = device + self.__output_cache = ResourceCacheDictTF(self._device) + self.__output_gradient_cache = ResourceCacheDictTF(self._device) + self.__input_cache = ResourceCacheDictTF(self._device) + self.__input_grad_cache = ResourceCacheDictTF(self._device) + self.clear_cache() + self.parent_state = parent_state + self.use_raw_images = True + self.volume_decoder = volume_decoder + + self.set_input_encoder(None) + self.set_downscale_encoder(None) + + assert velocity_format in ["CENTERED", "STAGGERED", "CURL_CENTERED", "CURL_STAGGERED"] + self.velocity_format = "CENTERED" if velocity_format in ["CENTERED", "CURL_CENTERED"] else "STAGGERED" #CENTERED, STAGGERED + + self.use_curl_potential = velocity_format in ["CURL_CENTERED", "CURL_STAGGERED"] #True + #if self.use_curl_potential and not velocity_format=="CENTERED": + # raise ValueError("When generating velocity via curl the velocity_format must be CENTERED.") + + # use potential for MS residual instead of velocity + self.residual_potential = True # interpolation when up-scaling vel causes noticeable divergence. so up-scale the potential instead to keep the velocity div-free + + if self.use_curl_potential: + # TODO expose curl settings in setup and remove this + LOG.info("Generating %s velocity via curl.", self.velocity_format) + + self.set_boundary(boundary) + self.scale_renderer = scale_renderer + self.warp_renderer = warp_renderer + + self.is_var = False + self._name = var_name + self._is_trainable = trainable + #raise NotImplementedError() + + self.recursive_MS = False + #self.input_current_features = True + + assert step_input_density==[0], "DEBUG" + self.step_input_density = step_input_density + assert step_input_density_target==[], "DEBUG" + self.step_input_density_target = step_input_density_target + #assert step_input_density_proxy==[], "DEBUG" + self.step_input_density_proxy = step_input_density_proxy + #assert step_input_features==[0,1], "DEBUG" + self.step_input_features = step_input_features + + # also check requires_parent_state_variables() if adding new feature inputs + assert all(_ in ["INPUT_IMAGES_UNPROJECTION","INPUT_IMAGES_UNPROJECTION_CONCAT","INPUT_IMAGES_RAW_UNPROJECTION","INPUT_IMAGES_HULL", "ENC3D", "ENCLIFT"] for _ in type_input_features) + #assert type_input_features==["ENC3D"], "DEBUG" + self.type_input_features = type_input_features + #assert warp_input_indices==[0,1], "DEBUG" + self.warp_input_indices = warp_input_indices + self.downscale_input_modes = downscale_input_modes + + self.norm_input_mode = "GROUP" #NONE, SINGLE, GROUP, ALL + + self.__centered_shape = None + + # self.__output_users = [] + # self.__output_gradient_cache = None + # self.__inputs_for_backprop = [] + + # backprop interface + def __add_used_input(self, other, output_id): + self.__inputs_for_backprop.append(input_key(other, output_id)) + + def __register_inputs(self): + for inp, out_id in self.__inputs_for_backprop: + assert isinstance(inp, BackpropInterface) + inp._register_output_user(self, out_id) + + def __gather_inputs(self): + return [inp.output(out_id) for inp, out_id in self.__inputs_for_backprop] + + def outputs(self): + return {"CENTERED": self.centered()} + #"STAGGERED_X": self._x + + def output(self, output_id="CENTERED"): + assert output_id=="CENTERED" + return self.outputs()[output_id] + + def _register_output_user(self, other, output_id): + assert isinstance(other, BackpropInterface) + if other not in self.__output_users[output_id]: + self.__output_users[output_id].append(other) + + def _compute_input_grads(self): + if self.__input_grad_cache.is_empty(): + assert self.__output_gradient_cache is not None + self.backprop_accumulate(**self.__output_gradient_cache) #**self.__get_output_grads()) # + self.__output_gradient_cache = None + + def _get_input_grad(self, other, output_id): + assert not self.__input_grad_cache.is_empty() + t = input_key(other, output_id) + idx = self.__inputs_for_backprop.index(t) + return self.__input_grad_cache["input_grad_%d"%(idx,)] + + def has_gradients_for(self, other, output_id): + return (input_key(other, output_id) in self.__inputs_for_backprop) and (not self.__input_grad_cache.is_empty() or self.can_backprop) + + @property + def can_backprop(self): + # has output gradients available + #LOG.info("'%s' can backprop: out grad %s; output users %d, provides grads %s", self._name, self.__output_gradient_cache is not None, len(self.__output_users), [_.has_gradients_for(self) for _ in self.__output_users]) + return (self.__output_gradient_cache is not None) or (len(self.__output_users)>0 and any(len(users)>0 and any(_.has_gradients_for(self, out_id) for _ in users) for out_id, users in self.__output_users.items())) + + @property + def requires_backprop(self): + is_frozen = all(_.is_frozen_weights for _ in self.volume_decoder) if self.has_multiple_decoders else self.volume_decoder.is_frozen_weights + return (not is_frozen) and self.__input_grad_cache.is_empty() and self.can_backprop + + # own + + @property + def parent_state(self): + return self.__state + @parent_state.setter + def parent_state(self, value): + assert is_None_or_type(value, NeuralState) + self.__state = value + + def _get_batch_size(self): + if self.__centered is not None: + return shape_list(self.__centered)[0] + elif self.parent_state is not None: + return self.parent_state._get_batch_size() + else: + raise RuntimeError("NeuralVelocityGrid: Can't determine batch size without cached velocity or parent state.") + #return self._batch_size + + @property + def lod_pad(self): + return tf.zeros([self._get_batch_size()]+self.centered_shape+[1]) + def set_centered_shape(self, centered_shape): + assert is_None_or_type(centered_shape, (list, tuple)) + if not centered_shape is None: + assert len(centered_shape)==3 + assert all(isinstance(_, Integral) for _ in centered_shape) + self.__centered_shape = centered_shape + @property + def centered_shape(self): + if self.__centered_shape is None: + if self.__state is None: raise ValueError("Parent state is not set") + return self.__state.transform.grid_size + else: + return self.__centered_shape + @property + def x_shape(self): + return self.component_shapes(self.centered_shape)[0] + @property + def y_shape(self): + return self.component_shapes(self.centered_shape)[1] + @property + def z_shape(self): + return self.component_shapes(self.centered_shape)[2] + + def set_input_encoder(self, input_encoder): + if isinstance(input_encoder, list): + assert all(isinstance(_, DeferredBackpropNetwork) for _ in input_encoder) + self.__shared_input_encoder = False + else: + assert is_None_or_type(input_encoder, DeferredBackpropNetwork) + self.__shared_input_encoder = True + self.__input_encoder = input_encoder + + @property + def _has_input_encoder(self): + return self.__input_encoder is not None + + def _get_input_encoder(self, scale): + if self.__shared_input_encoder: + return self.__input_encoder + else: + return self.__input_encoder[scale] + + @property + def active_input_encoders(self): + if not self.__shared_input_encoder: + return [self.__input_encoder[s] for s in self.gen_current_MS_scales()] + else: + return [self.__input_encoder] + + def set_downscale_encoder(self, encoder): + if isinstance(encoder, list): + assert all(isinstance(_, DeferredBackpropNetwork) for _ in encoder) + self.__shared_downscale_encoder = False + else: + assert is_None_or_type(encoder, DeferredBackpropNetwork) + self.__shared_downscale_encoder = True + self.__downscale_encoder = encoder + @property + def _has_downscale_encoder(self): + return self.__downscale_encoder is not None + def _get_downscale_encoder(self, scale): + if self.__shared_downscale_encoder: + return self.__downscale_encoder + else: + return self.__downscale_encoder[scale] + @property + def active_downscale_encoders(self): + if not self.__shared_downscale_encoder: + scales = list(self.gen_max_MS_scales() if self.recursive_MS_use_max_level_input else self.gen_current_MS_scales())[:-1] + return [self.__downscale_encoder[s] for s in scales] + else: + return [self.__downscale_encoder] + + def _get_generator_input(self): + if "BASE" not in self.__generator_inputs: + centered_shape = self.centered_shape #self.centered_shape + with SAMPLE("velocity input"): + inp = [] + feature_shape = None + for step in self.step_input_density: + state = self._get_state_by_step(step) + if state is not None: + #inp.append(state.density.output()) + inp.append(state.density.scaled(centered_shape, with_inflow=True)) + feature_shape = shape_list(inp[-1]) + assert has_shape(inp[-1], [None]+centered_shape+[None]), "%s, %s"%(shape_list(inp[-1]), [None]+centered_shape+[None]) + #self.__inputs_for_backprop.append(state.density) + self.__add_used_input(state.density, "OUTPUT") + else: + LOG.debug("NeuralVelocityGrid of frame %d is missing density input of state %d (step %d)", self.parent_state.frame, self.parent_state.frame+step, step) + inp.append(tf.zeros(feature_shape)) + + #feature_shape = None + for step in self.step_input_density_target: + state = self._get_state_by_step(step) + if state is not None: + raise NotImplementedError("Debug") + temp_inp = state.density_target.scaled(centered_shape, with_inflow=True) + inp.append(temp_inp) + ##feature_shape = shape_list(temp_inp) + del temp_inp + else: + LOG.debug("NeuralVelocityGrid of frame %d is missing density target input of state %d (step %d)", self.parent_state.frame, self.parent_state.frame+step, step) + inp.append(tf.zeros(feature_shape)) + + #feature_shape = None + for step in self.step_input_density_proxy: + state = self._get_state_by_step(step) + if state is not None and state.has_density_proxy: + inp.append(state.density_proxy.output("OUTPUT")) + feature_shape = shape_list(inp[-1]) + assert has_shape(inp[-1], [None]+centered_shape+[None]), "%s, %s"%(shape_list(inp[-1]), [None]+centered_shape+[None]) + #self.__inputs_for_backprop.append(state.density_proxy) + self.__add_used_input(state.density_proxy, "OUTPUT") + else: + # FOR TESTING + if state is not None: + raise RuntimeError("State %d is missing denstiy proxy as input for frame %d.", state.frame, self.parent_state.frame) + + LOG.debug("NeuralVelocityGrid of frame %d is missing density proxy input of state %d (step %d)", self.parent_state.frame, self.parent_state.frame+step, step) + inp.append(tf.zeros(feature_shape)) + + feature_shape = None + for step in self.step_input_features: + state = self._get_state_by_step(step) + if state is not None: + # assert not self.use_raw_images and self.type_input_features==["ENC3D"] + # inp.append(state.output()) + # feature_shape = shape_list(inp[-1]) + # assert has_shape(inp[-1], [None]+centered_shape+[None]), "%s, %s"%(shape_list(inp[-1]), [None]+centered_shape+[None]) + # self.__inputs_for_backprop.append(state) + + assert not self.use_raw_images + temp_inp = state.get_volume_features(types=self.type_input_features, shape=centered_shape, concat=False) + + if "ENC3D" in self.type_input_features: + temp_inp.append(state.output("OUTPUT")) + assert has_shape(temp_inp[-1], [None]+centered_shape+[None]), "%s, %s"%(shape_list(temp_inp[-1]), [None]+centered_shape+[None]) + #self.__inputs_for_backprop.append(state) + self.__add_used_input(state, "OUTPUT") + + inp.append(tf.concat(temp_inp, axis=-1)) + del temp_inp + feature_shape = shape_list(inp[-1]) + else: + LOG.debug("NeuralVelocityGrid of frame %d is missing feature input of state %d (step %d)", self.parent_state.frame, self.parent_state.frame+step, step) + inp.append(tf.zeros(feature_shape)) + del feature_shape + + self.__register_inputs() + + inp_stag = [] + + self.__generator_inputs["BASE"] = (inp, inp_stag) + + return self.__generator_inputs["BASE"] + + def _get_state_by_step(self, step): + assert isinstance(step, Integral) and step>=0 + state = self.parent_state + while step>0: + if state.next is None: + #raise AttributeError("Can't get next state") + return None + state = state.next + step -=1 + return state + + def get_advected(self, scale=None, allow_potential=True): + # return own self-advected velocity + raise NotImplementedError + if self.is_MS and scale is not None: + pass + else: + pass + + def _get_generator_input_MS(self, scale, cache_inputs=False): + recurrent_input_vel = False + recurrent_input_vel_MS = False #use all scales or just top? + recurrent_input_vel_potential = True #use generated potential instead of velocity if generating potentials + recurrent_input_vel_warp = False #try this? + # TODO: recursive_MS input generation; possibly multi-scale input/features provided by parent state + + + centered_scale_shape = self.centered_shape_of_scale(scale) #self.centered_shape + if scale not in self.__generator_inputs: + if (self.__is_top_scale(scale) if self.recursive_MS_use_max_level_input else self.__is_top_active_scale(scale)) or self._parent_provides_input_scale(scale): + + #LOG.info("Gather input for scale %d %s", scale, centered_scale_shape) + + with SAMPLE("velocity top MS input"): + inp = [] + feature_shape = None + for step in self.step_input_density: + state = self._get_state_by_step(step) + if state is not None: + # temp_inp = state.density.scaled(centered_scale_shape, with_inflow=True) + # temp_inp = tf.stop_gradient(temp_inp) + # inp.append(temp_inp) + # if cache_inputs: + # self.__input_cache[("density", state.frame, scale)] = temp_inp + # feature_shape = shape_list(temp_inp) + # del temp_inp + + #inp.append(state.density.output()) + inp.append(state.density.scaled(centered_scale_shape, with_inflow=True)) + feature_shape = shape_list(inp[-1]) + assert has_shape(inp[-1], [None]+centered_scale_shape+[None]), "%s, %s"%(shape_list(inp[-1]), [None]+centered_scale_shape+[None]) + #self.__inputs_for_backprop.append(state.density) + self.__add_used_input(state.density, "OUTPUT") + else: + LOG.debug("NeuralVelocityGrid of frame %d is missing density input of state %d (step %d)", self.parent_state.frame, self.parent_state.frame+step, step) + inp.append(tf.zeros(feature_shape)) + + #feature_shape = None + for step in self.step_input_density_target: + state = self._get_state_by_step(step) + if state is not None: + raise NotImplementedError("Debug") + temp_inp = state.density_target.scaled(centered_scale_shape, with_inflow=True) + inp.append(temp_inp) + if cache_inputs: + self.__input_cache[("density_target", state.frame, scale)] = temp_inp + ##feature_shape = shape_list(temp_inp) + del temp_inp + else: + LOG.debug("NeuralVelocityGrid of frame %d is missing density target input of state %d (step %d)", self.parent_state.frame, self.parent_state.frame+step, step) + inp.append(tf.zeros(feature_shape)) + + #feature_shape = None + for step in self.step_input_density_proxy: + state = self._get_state_by_step(step) + if state is not None and state.has_density_proxy: + # temp_inp = state.density_proxy.scaled(centered_scale_shape, with_inflow=True) + # temp_inp = tf.stop_gradient(temp_inp) + # inp.append(temp_inp) + # if cache_inputs: + # self.__input_cache[("density_proxy", state.frame, scale)] = temp_inp + # feature_shape = shape_list(temp_inp) + # del temp_inp + #inp.append(state.density_proxy.output()) + inp.append(state.density_proxy.scaled(centered_scale_shape, with_inflow=True)) + feature_shape = shape_list(inp[-1]) + assert has_shape(inp[-1], [None]+centered_scale_shape+[None]), "%s, %s"%(shape_list(inp[-1]), [None]+centered_scale_shape+[None]) + #self.__inputs_for_backprop.append(state.density_proxy) + self.__add_used_input(state.density_proxy, "OUTPUT") + else: + # FOR TESTING + if state is not None: + raise RuntimeError("State %d is missing denstiy proxy as input for frame %d.", state.frame, self.parent_state.frame) + + LOG.debug("NeuralVelocityGrid of frame %d is missing density proxy input of state %d (step %d)", self.parent_state.frame, self.parent_state.frame+step, step) + inp.append(tf.zeros(feature_shape)) + + feature_shape = None + for step in self.step_input_features: + state = self._get_state_by_step(step) + if state is not None: + # if self.use_raw_images: + # #inp.append(state.targets_raw_feature_volume) + # raise NotImplementedError + # #inp.append(state.get_volume_features(types=self.type_input_features)) + # #inp.append(tf.zeros_like(state.targets_raw_feature_volume)) + # else: + # temp_inp = state.get_volume_features(types=self.type_input_features, shape=centered_scale_shape) + # inp.append(temp_inp) + # if cache_inputs: + # self.__input_cache[("density_proxy", state.frame, scale)] = temp_inp + # feature_shape = shape_list(temp_inp) + # del temp_inp + # raise NotImplementedError("TODO:") + assert not self.use_raw_images + temp_inp = state.get_volume_features(types=self.type_input_features, shape=centered_scale_shape, concat=False) + + if "ENC3D" in self.type_input_features: + temp_inp.append(state.output("OUTPUT")) + assert has_shape(temp_inp[-1], [None]+centered_scale_shape+[None]), "%s, %s"%(shape_list(temp_inp[-1]), [None]+centered_scale_shape+[None]) + #self.__inputs_for_backprop.append(state) + self.__add_used_input(state, "OUTPUT") + + if len(temp_inp)>0: #could be empty due to external handling of ENCLIFT + inp.append(tf.concat(temp_inp, axis=-1)) + feature_shape = shape_list(inp[-1]) + else: + break + del temp_inp + else: + LOG.debug("NeuralVelocityGrid of frame %d is missing feature input of state %d (step %d)", self.parent_state.frame, self.parent_state.frame+step, step) + inp.append(tf.zeros(feature_shape)) + del feature_shape + + + self.__register_inputs() + + inp_stag = [] + if recurrent_input_vel and not (self.parent_state.prev.velocity.has_MS_output and recurrent_input_vel_MS): + if self.parent_state.prev is not None and self.parent_state.prev.has_velocity: + if recurrent_input_vel_warp: raise NotImplementedError + if self.parent_state.prev.velocity.use_curl_potential and recurrent_input_vel_potential: + temp_inp = self.parent_state.prev.velocity.__curl_potential_MS[scale] + inp_stag.append(temp_inp) + if cache_inputs: + self.__input_cache[("velocity_prev_curl_potential", state.frame, scale)] = temp_inp + elif self.is_staggered: + temp_inp = self.parent_state.prev.velocity._staggered(scale) + inp_stag.append(self._components_to_staggeredTensor(*temp_inp)) + if cache_inputs: + self.__input_cache[("velocity_prev_staggered_components", state.frame, scale)] = temp_inp + elif self.is_centered: + temp_inp = self.parent_state.prev.velocity.centered(scale) + if cache_inputs: + self.__input_cache[("velocity_prev_centered", state.frame, scale)] = temp_inp + inp.append(temp_inp) + del temp_inp + #if self.is_staggered: + # inp = [self._scalar_centered_to_staggered(_) for _ in inp] + else: + inp, inp_stag = self._get_generator_input_MS(self._get_larger_scale(scale), cache_inputs) + + #LOG.info("Scale input to scale %d %s", scale, centered_scale_shape) + + if "ENCLIFT" in self.type_input_features: + # cut features that are created per-level + inp = inp[:-len(self.step_input_features)] + + if self._has_downscale_encoder: + assert len(inp)==len(self.downscale_input_modes), "inputs do not match downscale modes." + inp = [self._scale_input_down(data, scale, mode) for data, mode in zip(inp, self.downscale_input_modes)] + else: + inp = [self._scale_input_down(data, scale, "RESAMPLE") for data in inp] + + # temp, TODO: handle on-scale creation vs. downsampling per-input + if "ENCLIFT" in self.type_input_features: + for step in self.step_input_features: + state = self._get_state_by_step(step) + if state is not None: + output_id = "LIFTING_"+("-".join(str(_) for _ in centered_scale_shape)) + temp_inp = state.output(output_id) + if self._has_input_encoder: + temp_inp = self._get_input_encoder(scale)(temp_inp) + inp.append(temp_inp) + self.__add_used_input(state, output_id) + feature_shape = shape_list(inp[-1]) + else: + inp.append(tf.zeros(feature_shape)) + del feature_shape + + if recurrent_input_vel and (self.parent_state.prev.velocity.has_MS_output and recurrent_input_vel_MS): + if self.parent_state.prev is not None: + raise NotImplementedError + if self.parent_state.prev.velocity.use_curl_potential and recurrent_input_vel_potential: + inp_stag.append(self.parent_state.prev.velocity.__curl_potential_MS[scale]) + elif self.is_staggered: + inp_stag.append(self.parent_state.prev.velocity._staggered_MS(scale)) + elif self.is_centered: + inp.append(self.parent_state.prev.velocity.centered_MS(scale)) + + if recurrent_input_vel and self.parent_state.prev is None: + raise NotImplementedError + if self.is_staggered: + inp_stag.append(tf.zeros(self.shape_of_scale(scale))) + elif self.is_centered: + inp.append(tf.zeros(self.shape_of_scale(scale))) + + + + self.__generator_inputs[scale] = (inp, inp_stag) + + return self.__generator_inputs[scale] + + def _get_top_scale(self): + return self.recursive_MS_scales[self.recursive_MS_max_level] + def _get_top_active_scale(self): + return self.recursive_MS_scales[self.recursive_MS_current_level] + def __is_top_scale(self, scale): + return scale==self.recursive_MS_scales[self.recursive_MS_max_level] + def __is_top_active_scale(self, scale): + return scale==self.recursive_MS_scales[self.recursive_MS_current_level] + def _parent_provides_input_scale(self, scale): + return self.recursive_MS_direct_input #False + def _get_larger_scale(self, scale): + return self.recursive_MS_scales[self.recursive_MS_scales.index(scale)+1] + + def set_recursive_MS(self, num_scales: Integral, scale_factor: Number, shared_decoder=True, train_mode="ALL", shapes=None, shape_cast_fn=round, direct_input=False, max_level_input=False): + self.recursive_MS = True + self.recursive_MS_direct_input = direct_input + self.recursive_MS_max_level = num_scales-1 + self.recursive_MS_current_level = self.recursive_MS_max_level + self.recursive_MS_use_max_level_input = max_level_input + assert isinstance(scale_factor, Number) + if scale_factor<=0: LOG.warning("recursive MS scale factor should be positive. is: %s", scale_factor) + self.recursive_MS_scale_factor = scale_factor + self.recursive_MS_scale_magnitude = True + self.recursive_MS_scales = list(range(num_scales)) + self.recursive_MS_shape_cast_fn = shape_cast_fn + if shapes is None: + self._recursive_MS_shapes = list(reversed([[int(shape_cast_fn(_/(self.recursive_MS_scale_factor**scale))) for _ in self.centered_shape] for scale in self.recursive_MS_scales])) + else: + assert isinstance(shapes, list) and len(shapes)==num_scales + assert all(isinstance(shape, list) for shape in shapes) + assert all(has_shape(_, shape_list(self.centered_shape)) for _ in shapes) + if not all(shapes[-1]==self.centered_shape): LOG.warning("Maximum resolution of shapes provided for recursive MS {} does not match main resolution of this NeuralVelocityGrid {}".format(shapes[-1], self.centered_shape)) + if not all(all(shapes[i]<=shapes[+1]) for i in range(len(shapes)-1)): LOG.warning("Shapes provided for recursive MS are not growing in all dimensions: {}".format(shapes)) + self._recursive_MS_shapes = copy.deepcopy(shapes) + #self.set_recursive_MS_level(num_scales, scale_factor, shapes, shape_cast_fn) + + self.recursive_MS_input_vel = True + if shared_decoder: + assert isinstance(self.volume_decoder, (DeferredBackpropNetwork, tf.keras.Model)) + else: + assert isinstance(self.volume_decoder, list) \ + and len(self.volume_decoder)==(self.recursive_MS_max_level+1) \ + and all(isinstance(_, (DeferredBackpropNetwork, tf.keras.Model)) for _ in self.volume_decoder) + self.recursive_MS_shared_decoder = shared_decoder + assert train_mode in ["ALL", "TOP"] + self.recursive_MS_train_mode = train_mode #"ALL" if self.recursive_MS_shared_decoder else "TOP" #ALL, TOP; no effect with shared decoder + self.cache_MS = True + + LOG.info("Setup recursive multi-scale for NeuralVelocityGrid of frame %s with %d scales (%s), factor %f, %s decoders, training '%s'. The output scales are%s cached.", \ + "?" if self.parent_state is None else self.parent_state.frame, self.num_recursive_MS_scales, self.recursive_MS_shapes, self.recursive_MS_scale_factor, \ + "shared" if self.recursive_MS_shared_decoder else "separate", self.recursive_MS_train_mode, "" if self.has_MS_output else " not") + + if self.recursive_MS_shared_decoder and self.recursive_MS_train_mode!="ALL": + LOG.warning("recursive_MS_train_mode 'TOP' has no effect on shared decoder.") + + self.recursive_MS_residual_weights = [1.0 for _ in range(num_scales)] + + @property + def recursive_MS_shapes(self): + #if self.recursive_MS_shared_decoder: + # return list(reversed([[int(self.recursive_MS_shape_cast_fn(_/(self.recursive_MS_scale_factor**scale))) for _ in self.centered_shape] for scale in self.gen_max_MS_scales()])) + #else: + return self._recursive_MS_shapes + + def set_recursive_MS_level(self, level: Integral, copy_weights: bool = False): + if not self.recursive_MS: raise RuntimeError("recursive_MS was not set up.") + if level<0 or self.recursive_MS_max_level0: + inputs_frames.insert(0, self.__normalize_tensor(tf.concat(dens_inputs, axis=-1))) + del dens_inputs + + if num_feature_inputs>0: + inputs_frames.insert(1, self.__normalize_tensor(tf.concat(features_inputs, axis=-1))) + del features_inputs + + elif self.norm_input_mode=="ALL" and (num_density_inputs+num_feature_inputs)>0: + inputs_frames = [self.__normalize_tensor(tf.concat(inputs_frames[:num_density_inputs+num_feature_inputs], axis=-1))] + inputs_frames[num_density_inputs+num_feature_inputs:] + + with SAMPLE("make input"): + # vel is also input + if self.recursive_MS_input_vel: + inputs_frames.append(vel) + + #LOG.info("input stats: %s", [(tf.reduce_mean(_).numpy().tolist(), tf.reduce_min(_).numpy().tolist(), tf.reduce_max(_).numpy().tolist()) for _ in inputs_frames]) + inputs_frames = tf.concat(inputs_frames, axis=-1) + + with SAMPLE("volume_decoder"): + if self.recursive_MS_shared_decoder: + vel_residual = self.volume_decoder(inputs_frames) + else: + vel_residual = self.volume_decoder[s](inputs_frames) + + with SAMPLE("make output"): + # https://de.wikipedia.org/wiki/Rotation_eines_Vektorfeldes#Rechenregeln + # curl(c*F + G) = c*curl(F) + curl(G) + # => could use residual potential instead of (or in addition to) residual velocity + if self.use_curl_potential: + if not self.residual_potential: + vel_residual = self._centered_to_curl(vel_residual) if self.is_centered else self._staggeredTensor_potential_to_staggeredTensor(vel_residual) #turn potential into divergence-free velocity + vel = vel + self.recursive_MS_residual_weights[s] * vel_residual + else: + #warnings.warn("Only using scaled min scale vel!") + #if s is active_MS_scales[0]: + vel = vel + self.recursive_MS_residual_weights[s] * vel_residual + vel_potential = vel + #vel_residual_potential = vel_residual + + vel = self._centered_to_curl(vel) if self.is_centered else self._staggeredTensor_potential_to_staggeredTensor(vel) + vel_residual = self._centered_to_curl(vel_residual) if self.is_centered else self._staggeredTensor_potential_to_staggeredTensor(vel_residual) + else: + #vel += vel_residual + vel = vel + self.recursive_MS_residual_weights[s] * vel_residual + + else: + vel_residual = tf.zeros_like(vel) + if self.use_curl_potential and self.residual_potential: + vel_potential = vel + vel = self._centered_to_curl(vel) if self.is_centered else self._staggeredTensor_potential_to_staggeredTensor(vel) + + if self.cache_MS: + with SAMPLE("cache"): + if self.is_centered: + with tf.device(self._device): + self.__centered_MS[s] = tf.identity(vel) + self.__centered_MS_residual[s] = tf.identity(vel_residual) + if self.is_staggered: + tmp = self._staggeredTensor_to_components(vel) + with tf.device(self._device): + self.__staggered_MS[s] = [tf.identity(_) for _ in tmp] + tmp = self._staggeredTensor_to_components(vel_residual) + with tf.device(self._device): + self.__staggered_MS_residual[s] = [tf.identity(_) for _ in tmp] + del tmp + # if self.use_curl_potential: + # with tf.device(self._device): + # self.__curl_potential_MS[s] = vel_potential + # self.__output_cache[("curl_potential_MS",s)] = vel_potential + + if self.use_curl_potential and self.residual_potential and s is not active_MS_scales[-1]: + vel = vel_potential + + # END scale profilie + + # END scale loop + + self.clear_input_cache() #? + #with tf.device(self._device): + # self.__curl_potential = tf.identity(vel_potential) if self.is_centered else [tf.identity(_) for _ in vel_potential] + v = vel #if self.centered else self._staggeredTensor_to_components(vel) + else: + inputs_frames, inputs_frames_stag = self._get_generator_input() #list of inputs for current level/scale + + with SAMPLE("transform format"): + #transform to staggered after warp + if self.is_staggered: + inputs_frames = [self._scalar_centered_to_staggered(_, allow_split_channels=True) for _ in inputs_frames] #resample to .5 shifted grid with dim+1 + inputs_frames += inputs_frames_stag + if self.is_centered: + inputs_frames += [self._staggered_to_centered(_) for _ in inputs_frames_stag] + + with SAMPLE("normalize"): + num_density_inputs = len(self.step_input_density) + len(self.step_input_density_target) + len(self.step_input_density_proxy) + num_feature_inputs = len(self.step_input_features) + if self.norm_input_mode=="SINGLE": + #inputs_frames = map(normalize_tensor, inputs_frames) + for idx in range(num_density_inputs+num_feature_inputs): + # we don't want to normalize velocities + inputs_frames[idx] = self.__normalize_tensor(inputs_frames[idx]) + + elif self.norm_input_mode=="GROUP": + #inputs_frames at this point: densities, features, recurrent velocity + dens_inputs = inputs_frames[:num_density_inputs] + features_inputs = inputs_frames[num_density_inputs:num_density_inputs+num_feature_inputs] + inputs_frames = inputs_frames[num_density_inputs+num_feature_inputs:] + + if num_density_inputs>0: + inputs_frames.insert(0, self.__normalize_tensor(tf.concat(dens_inputs, axis=-1))) + del dens_inputs + + if num_feature_inputs>0: + inputs_frames.insert(1, self.__normalize_tensor(tf.concat(features_inputs, axis=-1))) + del features_inputs + + elif self.norm_input_mode=="ALL" and (num_density_inputs+num_feature_inputs)>0: + inputs_frames = [self.__normalize_tensor(tf.concat(inputs_frames[:num_density_inputs+num_feature_inputs], axis=-1))] + inputs_frames[num_density_inputs+num_feature_inputs:] + + with SAMPLE("make input"): + #LOG.info("input stats: %s", [(tf.reduce_mean(_).numpy().tolist(), tf.reduce_min(_).numpy().tolist(), tf.reduce_max(_).numpy().tolist()) for _ in inputs_frames]) + inputs_frames = tf.concat(inputs_frames, axis=-1) + + with SAMPLE("volume_decoder"): + vel = self.volume_decoder(inputs_frames) + + with SAMPLE("make output"): + # https://de.wikipedia.org/wiki/Rotation_eines_Vektorfeldes#Rechenregeln + # curl(c*F + G) = c*curl(F) + curl(G) + # => could use residual potential instead of (or in addition to) residual velocity + if self.use_curl_potential: + vel = self._centered_to_curl(vel) if self.is_centered else self._staggeredTensor_potential_to_staggeredTensor(vel) + + self.clear_input_cache() #? + v = vel #if self.centered else self._staggeredTensor_to_components(vel) + + if self.is_staggered: + with SAMPLE("make components"): + v = self._staggeredTensor_to_components(v) + #d = grad_log(d, "generate_denstiy end", LOG.info) + return v + + + def centered(self, pad_lod=False, concat=True): + if self.__centered is not None: + v = self.__centered + else: + if self.is_centered: + v = self.generate_velocity() + elif self.is_staggered: + v = super().centered(pad_lod=pad_lod, concat=True) + + if self.cache_output: + with tf.device(self._device): + self.__centered = tf.identity(v) + v = self.__centered + if pad_lod: + raise NotImplementedError("pad_lod is not supported, it would conflict with caching and gradient calculations.") + if not concat: + #raise NotImplementedError("not concat is not supported, it would conflict with caching and gradient calculations.") + return tf.split(v, 3, axis=-1) + return v + + def centered_MS(self, scale, pad_lod=False, concat=True): + if not self.has_MS_output: raise ValueError("Mutiscale velocity is not available.") + #if not self.is_centered: raise NotImplementedError("accessor staggered MS not implemented.") + if self.is_centered: + if self.__centered is not None: + if scale not in self.__centered_MS: + raise ValueError("Mutiscale velocity of scale \"{}\" is not available. Available scales: {}".format(scale, list(self.__centered_MS.keys()))) + else: + if scale not in self.recursive_MS_scales: + raise ValueError("Mutiscale velocity of scale \"{}\" is not available. Available scales: {}".format(scale, self.recursive_MS_scales)) + self.centered() + v = self.__centered_MS[scale] + elif self.is_staggered: + if scale not in self.__centered_MS: + v = self._components_to_centered(*self._staggered_MS(scale)) + with tf.device(self._device): + self.__centered_MS[scale] = tf.identity(v) + v = self.__centered_MS[scale] + if pad_lod: + raise NotImplementedError("pad_lod is not supported, it would conflict with caching and gradient calculations.") + if not concat: + #raise NotImplementedError("not concat is not supported, it would conflict with caching and gradient calculations.") + return tf.split(v, 3, axis=-1) + return v + def centered_MS_residual(self, scale, pad_lod=False, concat=True): + if not self.has_MS_output: raise ValueError("Mutiscale velocity is not available.") + #if not self.is_centered: raise NotImplementedError("accessor staggered MS not implemented.") + if self.is_centered: + if self.__centered is not None: + if scale not in self.__centered_MS_residual: + raise ValueError("Mutiscale velocity of scale \"{}\" is not available. Available scales: {}".format(scale, list(self.__centered_MS_residual.keys()))) + else: + if scale not in self.recursive_MS_scales: + raise ValueError("Mutiscale velocity of scale \"{}\" is not available. Available scales: {}".format(scale, self.recursive_MS_scales)) + self.centered() + v = self.__centered_MS_residual[scale] + elif self.is_staggered: + if scale not in self.__centered_MS_residual: + v = self._components_to_centered(*self._staggered_MS_residual(scale)) + with tf.device(self._device): + self.__centered_MS_residual[scale] = tf.identity(v) + v = self.__centered_MS_residual[scale] + if pad_lod: + raise NotImplementedError("pad_lod is not supported, it would conflict with caching and gradient calculations.") + if not concat: + #raise NotImplementedError("not concat is not supported, it would conflict with caching and gradient calculations.") + return tf.split(v, 3, axis=-1) + return v + + def _staggered(self): + if self.__staggered is not None: + v = self.__staggered + else: + if self.is_staggered: + v = self.generate_velocity() + elif self.is_centered: + v = self._centered_to_staggered(self.centered()) + + if self.cache_output: + with tf.device(self._device): + self.__staggered = [tf.identity(_) for _ in v] + v = self.__staggered + return v + @property + def _x(self): + return self._staggered()[0] + @property + def _y(self): + return self._staggered()[1] + @property + def _z(self): + return self._staggered()[2] + + def _staggered_MS(self, scale): + if not self.has_MS_output: raise ValueError("Mutiscale velocity is not available.") + #if not self.is_centered: raise NotImplementedError("accessor staggered MS not implemented.") + if self.is_staggered: + # check if base vel has been generated. if not, do so to also generate the MS scales. + if self.__staggered is not None: + if scale not in self.__staggered_MS: #velocity has been generated, but the scale is (still) not available + raise ValueError("Mutiscale velocity of scale \"{}\" is not available. Available scales: {}".format(scale, list(self.__staggered_MS.keys()))) + else: + if scale not in self.recursive_MS_scales: + raise ValueError("Mutiscale velocity of scale \"{}\" is not available. Available scales: {}".format(scale, self.recursive_MS_scales)) + self._staggered() + v = self.__staggered_MS[scale] + elif self.is_centered: + if scale not in self.__staggered_MS: + v = self._centered_to_staggered(self.centered_MS(scale)) + with tf.device(self._device): + self.__staggered_MS[scale] = [tf.identity(_) for _ in v] + v = self.__staggered_MS[scale] + return v + def _staggered_MS_residual(self, scale): + if not self.has_MS_output: raise ValueError("Mutiscale velocity is not available.") + #if not self.is_centered: raise NotImplementedError("accessor staggered MS not implemented.") + if self.is_staggered: + if self.__staggered is not None: + if scale not in self.__staggered_MS_residual: + raise ValueError("Mutiscale velocity of scale \"{}\" is not available. Available scales: {}".format(scale, list(self.__staggered_MS_residual.keys()))) + else: + if scale not in self.recursive_MS_scales: + raise ValueError("Mutiscale velocity of scale \"{}\" is not available. Available scales: {}".format(scale, self.recursive_MS_scales)) + self._staggered() + v = self.__staggered_MS_residual[scale] + elif self.is_centered: + if scale not in self.__staggered_MS_residual: + v = self._centered_to_staggered(self.centered_MS_residual(scale)) + with tf.device(self._device): + self.__staggered_MS_residual[scale] = [tf.identity(_) for _ in v] + v = self.__staggered_MS_residual[scale] + return v + + def _x_MS(self, scale): + return self._staggered_MS(scale)[0] + def _y_MS(self, scale): + return self._staggered_MS(scale)[1] + def _z_MS(self, scale): + return self._staggered_MS(scale)[2] + def x_MS(self, scale): + if self.boundary is not None: raise NotImplementedError("Boundaries not available for multi-scale velocity.") + return self._x_MS(scale) + def y_MS(self, scale): + if self.boundary is not None: raise NotImplementedError("Boundaries not available for multi-scale velocity.") + return self._y_MS(scale) + def z_MS(self, scale): + if self.boundary is not None: raise NotImplementedError("Boundaries not available for multi-scale velocity.") + return self._z_MS(scale) + + def _x_MS_r(self, scale): + return self._staggered_MS_residual(scale)[0] + def _y_MS_r(self, scale): + return self._staggered_MS_residual(scale)[1] + def _z_MS_r(self, scale): + return self._staggered_MS_residual(scale)[2] + def x_MS_r(self, scale): + if self.boundary is not None: raise NotImplementedError("Boundaries not available for multi-scale velocity.") + return self._x_MS_r(scale) + def y_MS_r(self, scale): + if self.boundary is not None: raise NotImplementedError("Boundaries not available for multi-scale velocity.") + return self._y_MS_r(scale) + def z_MS_r(self, scale): + if self.boundary is not None: raise NotImplementedError("Boundaries not available for multi-scale velocity.") + return self._z_MS_r(scale) + + + def divergence_MS(self, scale, world_scale=[1,1,1]): + #out - in per cell, per axis + x_div = self.x_MS(scale)[:,:,:,1:,:] - self.x_MS(scale)[:,:,:,:-1,:] + y_div = self.y_MS(scale)[:,:,1:,:,:] - self.y_MS(scale)[:,:,:-1,:,:] + z_div = self.z_MS(scale)[:,1:,:,:,:] - self.z_MS(scale)[:,:-1,:,:,:] + # sum to get total divergence per cell + div = x_div*world_scale[0]+y_div*world_scale[1]+z_div*world_scale[2] + return div + + def divergence_MS_residual(self, scale, world_scale=[1,1,1]): + #out - in per cell, per axis + x_div = self.x_MS_r(scale)[:,:,:,1:,:] - self.x_MS_r(scale)[:,:,:,:-1,:] + y_div = self.y_MS_r(scale)[:,:,1:,:,:] - self.y_MS_r(scale)[:,:,:-1,:,:] + z_div = self.z_MS_r(scale)[:,1:,:,:,:] - self.z_MS_r(scale)[:,:-1,:,:,:] + # sum to get total divergence per cell + div = x_div*world_scale[0]+y_div*world_scale[1]+z_div*world_scale[2] + return div + + def magnitude_MS(self, scale, world_scale=[1,1,1]): + with self.warp_renderer.profiler.sample("magnitude"): + v = self.centered_MS(scale, pad_lod=False)*tf.constant(world_scale, dtype=tf.float32) + return tf_norm2(v, axis=-1, keepdims=True) #tf.norm(v, axis=-1, keepdims=True) + + def magnitude_MS_residual(self, scale, world_scale=[1,1,1]): + with self.warp_renderer.profiler.sample("magnitude"): + v = self.centered_MS_residual(scale, pad_lod=False)*tf.constant(world_scale, dtype=tf.float32) + return tf_norm2(v, axis=-1, keepdims=True) #tf.norm(v, axis=-1, keepdims=True) + + + def var_list(self): + raise AttributeError("use .get_variables().") + + @property + def requires_parent_state_variables(self): + return False #"ENC3D" in self.type_input_features + + #def _get_variables_per_decoder(self): + # pass + def get_variables(self, scale=None): + if self.has_multiple_decoders: #recursive_MS and (not self.recursive_MS_shared_decoder): + if scale is None: + if self.recursive_MS_train_mode=="ALL": + var_dict = {'velocity_decoder': [var for dec in self.active_decoders for var in dec.trainable_variables]} + elif self.recursive_MS_train_mode=="TOP": + var_dict = {'velocity_decoder': self.volume_decoder[self.recursive_MS_scales[self.recursive_MS_current_level]].trainable_variables} + else: raise ValueError + else: + var_dict = {'velocity_decoder': self.volume_decoder[scale].trainable_variables} + else: + var_dict = {'velocity_decoder': self.volume_decoder.trainable_variables} + if self._has_input_encoder: + var_dict["input_encoder"] = [var for enc in self.active_input_encoders for var in enc.trainable_variables] + if self._has_downscale_encoder: + var_dict["downscale_encoder"] = [var for enc in self.active_downscale_encoders for var in enc.trainable_variables] + #if self.requires_parent_state_variables and self.parent_state is not None: + # var_dict.update(self.parent_state.get_variables()) + return var_dict + + # def get_input_variables(self): + # # output tensors of other Grid objects that are used as input to this network + # # density output of state.density, state.density_proxy + # # velocity and curl potential of state.velocity + # # volume feature tensor of state + # # of all used MS scales that are not generated by _get_generator_input_MS via downscaling + # raise NotImplementedError + # var_inp = {} + # if scale not in self.__generator_inputs: + # if self._is_top_scale(scale) or self._parent_provides_input_scale(scale): + # # input may not be generated and cached yet + # self._get_generator_input_MS(scale) + + + def _scatter_generator_input_gradients(self, input_gradients): + raise NotImplementedError + recurrent_input_vel = False + recurrent_input_vel_MS = False #use all scales or just top? + recurrent_input_vel_potential = True #use generated potential instead of velocity if generating potentials + warp_recurrent_vel = False #try this? + # TODO: recursive_MS input generation; possibly multi-scale input/features provided by parent state + if scale not in self.__generator_inputs: + if self._is_top_scale(scale) or self._parent_provides_input_scale(scale): + with SAMPLE("velocity top MS input"): + i = 0 + for step in self.step_input_density: + state = self._get_state_by_step(step) + if state is not None: + #inp.append(state.density.scaled(self.centered_shape, with_inflow=True)) + state.density.add_output_gradients(input_gradients[scale][i]) + i+=1 + + for step in self.step_input_density_target: + state = self._get_state_by_step(step) + if state is not None: + # inp.append(state.density_target.scaled(self.centered_shape, with_inflow=True)) + state.density_target.add_output_gradients(input_gradients[scale][i]) + i+=1 + + for step in self.step_input_density_proxy: + state = self._get_state_by_step(step) + if state is not None and state.has_density_proxy: + # inp.append(state.density_proxy.scaled(self.centered_shape, with_inflow=True)) + state.density_proxy.add_output_gradients(input_gradients[scale][i]) + i+=1 + + for step in self.step_input_features: + state = self._get_state_by_step(step) + if state is not None: + if self.use_raw_images: + raise NotImplementedError + else: + #inp.append(state.get_volume_features(types=self.type_input_features)) + state.add_volume_feature_gradients(input_gradients[scale][i], types=self.type_input_features) + i+=1 + + + inp_stag = [] + if recurrent_input_vel and not (self.parent_state.prev.velocity.has_MS_output and recurrent_input_vel_MS): + if self.parent_state.prev is not None: + self.parent_state.prev.velocity.add_output_gradients(input_gradients[scale][i]) + # if self.parent_state.prev.velocity.use_curl_potential and recurrent_input_vel_potential: + # inp_stag.append(self.parent_state.prev.velocity.__curl_potential_MS[scale]) + # elif self.is_staggered: + # inp_stag.append(self._components_to_staggeredTensor(*self.parent_state.prev.velocity._staggered(scale))) + # elif self.is_centered: + # inp.append(self.parent_state.prev.velocity.centered(scale)) + + # if recurrent_input_vel and (self.parent_state.prev.velocity.has_MS_output and recurrent_input_vel_MS): + # if self.parent_state.prev is not None: + # if self.parent_state.prev.velocity.use_curl_potential and recurrent_input_vel_potential: + # inp_stag.append(self.parent_state.prev.velocity.__curl_potential_MS[scale]) + # elif self.is_staggered: + # inp_stag.append(self.parent_state.prev.velocity._staggered_MS(scale)) + # elif self.is_centered: + # inp.append(self.parent_state.prev.velocity.centered_MS(scale)) + + # if recurrent_input_vel and self.parent_state.prev is None: + # if self.is_staggered: + # inp_stag.append(tf.zeros(self.shape_of_scale(scale))) + # elif self.is_centered: + # inp.append(tf.zeros(self.shape_of_scale(scale))) + + + def __get_MS_variable_keys_scale(self, scale, centered=True, staggered=True, include_residual=False): + key_list = [] + if centered: + key_list.append('velocity_%s_c'%(scale,)) + if include_residual: + key_list.append('velocity_r%s_c'%(scale,)) + if staggered: + key_list.append('velocity_%s_x'%(scale,)) + key_list.append('velocity_%s_y'%(scale,)) + key_list.append('velocity_%s_z'%(scale,)) + if include_residual: + key_list.append('velocity_r%s_x'%(scale,)) + key_list.append('velocity_r%s_y'%(scale,)) + key_list.append('velocity_r%s_z'%(scale,)) + + return key_list + + def _get_output_variable_keys(self, centered=True, staggered=True, include_MS=False, include_residual=False): + key_list = [] + if include_MS: + for scale in self.gen_current_MS_scales(): + key_list.extend(self.__get_MS_variable_keys_scale(scale, centered=centered, staggered=staggered, include_residual=include_residual)) + else: + if centered: + key_list.append('velocity_c') + if staggered: + key_list.append('velocity_x') + key_list.append('velocity_y') + key_list.append('velocity_z') + + return key_list + + def get_output_variables(self, centered=True, staggered=True, include_MS=False, include_residual=False, only_trainable=False): + if not self.cache_output: + raise RuntimeError("Output caching must be enabled for output variables to have meaning.") + var_dict = {} + if include_MS and self.has_MS_output: + scales = list(self.gen_current_trainable_MS_scales()) if only_trainable else list(self.gen_current_MS_scales()) + for scale in scales: + if centered: + var_dict['velocity_%s_c'%(scale,)] = self.centered_MS(scale) + if include_residual: + var_dict['velocity_r%s_c'%(scale,)] = self.centered_MS_residual(scale) + if staggered: + var_dict['velocity_%s_x'%(scale,)] = self._x_MS(scale) + var_dict['velocity_%s_y'%(scale,)] = self._y_MS(scale) + var_dict['velocity_%s_z'%(scale,)] = self._z_MS(scale) + if include_residual: + var_dict['velocity_r%s_x'%(scale,)] = self._x_MS_r(scale) + var_dict['velocity_r%s_y'%(scale,)] = self._y_MS_r(scale) + var_dict['velocity_r%s_z'%(scale,)] = self._z_MS_r(scale) + else: + if centered: + var_dict['velocity_c'] = self.centered() + if staggered: + var_dict['velocity_x'] = self._x + var_dict['velocity_y'] = self._y + var_dict['velocity_z'] = self._z + + return var_dict + + def map_gradient_output_to_MS(self, grad_dict): + assert isinstance(grad_dict, dict) + output_keys = self._get_output_variable_keys(centered=True, staggered=True, include_MS=False) + MS_keys = self.__get_MS_variable_keys_scale(scale=self._get_top_active_scale(), centered=True, staggered=True, include_residual=False) + assert len(output_keys)==len(MS_keys) + #also assume same order: c, x, y, z + assert all(a.endswith(b[-2:]) for a,b in zip(output_keys, MS_keys)) + out_dict = {} + for key, grad in grad_dict.items(): + if key not in output_keys: raise KeyError("Invalid output gradient key '{}'".format(key)) + out_dict[MS_keys[output_keys.index(key)]] = grad + return out_dict + + def assign(self, x,y,z): + raise TypeError("Can't assign to NeuralVelocityGrid") + + def assign_add(self, x,y,z): + raise TypeError("Can't assign_add to NeuralVelocityGrid") + + def assign_sub(self, x,y,z): + raise TypeError("Can't assign_sub to NeuralVelocityGrid") + + # def _cache(self, key, value): + # self.__output_cache[key] = value + + def clear_cache(self): + self.__output_cache.clear() + self.__centered = None + self.__centered_MS = {} + self.__centered_MS_residual = {} + self.__staggered = None + self.__staggered_MS = {} + self.__staggered_MS_residual = {} + self.clear_input_cache() + + self.__output_users = {"CENTERED": []} + self.__output_gradient_cache = None + self.__inputs_for_backprop = [] + self.__input_grad_cache.clear() + + def clear_input_cache(self): + self.__generator_inputs = {} + self.__input_cache.clear() + + def clear_cache_for_backprop(self): + # self.clear_cache() + # if self.requires_parent_state_variables: + # self.parent_state.clear_cache_for_backprop() + self.__output_cache.clear() + #self.__curl_potential = None + #self.__curl_potential_MS = {} + self.__centered = None + self.__centered_MS = {} + self.__centered_MS_residual = {} + self.__staggered = None + self.__staggered_MS = {} + self.__staggered_MS_residual = {} + self.clear_input_cache() + + def inspect_output_gradient_stats(self, opt_ctx): + if self.__output_gradient_cache is not None and "output_gradients" in self.__output_gradient_cache: + for name, grad in self.__output_gradient_cache["output_gradients"].tiems(): + opt_ctx.inspect_gradients_func(opt_ctx=opt_ctx, gradients=grad, name="vel_out/"+name) + opt_ctx.inspect_gradients_func(opt_ctx=opt_ctx, gradients=self._get_output_users_grads()["CENTERED"], name="velc_out/users") + + def add_output_gradients(self, output_gradients): + raise NotImplementedError + if not isinstance(output_gradients, dict): raise TypeError + for key, grad in output_gradients.items(): + if key in self.__output_gradient_cache: + grad = grad + self.__output_gradient_cache[key] + self.__output_gradient_cache[key] = grad + + # def __get_output_grads(self): + # if self.__output_gradient_cache is not None and "output_gradients" in self.__output_gradient_cache: + # return self.__output_gradient_cache + # else: + # out_vars = self.get_output_variables(True, True, self.is_MS, True, True) + # out_grads = { + # "output_gradients": {name: tf.zeros_like(var) for name, var in out_vars.items()} + # "include_MS": self.is_MS, + # "include_residual": True, + # "only_trainable": True, + # } + # return out_grads + + def _get_output_users_grads(self): + for out_id, users in self.__output_users.items(): + for other in users: + other._compute_input_grads() + extra_output_gradients = {} + for out_id, users in self.__output_users.items(): + extra_output_gradients[out_id] = tf.zeros_like(self.output(out_id)) + for other in users: + extra_output_gradients[out_id] = extra_output_gradients[out_id] + other._get_input_grad(self, out_id) + + # extra_output_gradients = tf.zeros_like(self.output()) + # if self.__output_users is not None and len(self.__output_users)>0: + # assert len(self.__output_users)==1 + # assert isinstance(self.__output_users[0], WarpedDensityGrid) + # for other in self.__output_users: + # other._compute_input_grads() + # for other in self.__output_users: + # extra_output_gradients = extra_output_gradients + other._get_input_grad(self) + return extra_output_gradients + + def backprop(self, output_gradients, include_MS=False, include_residual=False, only_trainable=False, provide_input_gradients=False, keep_output_gradients=False): + + #assert output_gradients is None, "DEBUG" + + check_output_users(self.__output_users, {"CENTERED": [WarpedDensityGrid]}, "NeuralVelocityGrid") + + extra_output_gradients = {"velocity_c":self._get_output_users_grads()["CENTERED"]} + if include_MS: + extra_output_gradients = self.map_gradient_output_to_MS(extra_output_gradients) + + for k in output_gradients: + if k in extra_output_gradients: + if output_gradients[k] is None: + output_gradients[k] = extra_output_gradients[k] + else: + output_gradients[k] += extra_output_gradients[k] + + for k in extra_output_gradients: + if k not in output_gradients: + raise KeyError("could not map extra output gradient '%s'."%(k,)) + + LOG.debug("Backprop velocity frame %d", self.parent_state.frame) + with SAMPLE("NVG backprop"): + if not include_MS and not (('velocity_c' in output_gradients) or \ + ('velocity_x' in output_gradients and 'velocity_y' in output_gradients and 'velocity_z' in output_gradients) \ + ): + raise ValueError("No gradients to backprop.") + if include_MS: + for scale in (self.gen_current_trainable_MS_scales() if only_trainable else self.gen_current_MS_scales()): + if not (('velocity_%s_c'%(scale,) in output_gradients) or \ + ('velocity_%s_x'%(scale,) in output_gradients and 'velocity_%s_y'%(scale,) in output_gradients and 'velocity_%s_z'%(scale,) in output_gradients)) \ + or ( \ + include_residual and not (('velocity_r%s_c'%(scale,) in output_gradients) or \ + ('velocity_r%s_x'%(scale,) in output_gradients and 'velocity_r%s_y'%(scale,) in output_gradients and 'velocity_r%s_z'%(scale,) in output_gradients)) \ + ): + raise ValueError("No gradients to backprop for MS scale %s."%(scale,)) + self.clear_cache_for_backprop() + var_dict = self.get_variables() + var_dict["inputs_for_backprop"] = self.__gather_inputs() + + if provide_input_gradients: + var_dict.update(self.get_input_variables()) + + with SAMPLE("forward"), tf.GradientTape(watch_accessed_variables=False) as tape: + tape.watch(var_dict) + output = self.get_output_variables(include_MS=include_MS, include_residual=include_residual) + + output = {k: output[k] for k in output if (k in output_gradients)} + output_gradients = { + k: (output_gradients[k] if output_gradients[k] is not None else tf.zeros_like(output[k])) \ + for k in output_gradients \ + if k in output \ + } + + # for k in output: + # LOG.info("vel output '%s': out type=%s, grad type=%s", k, output[k].__class__.__name__, output_gradients[k].__class__.__name__) + with SAMPLE("gradients"): + gradients = tape.gradient(output, var_dict, output_gradients=output_gradients) + + for i, grad in enumerate(gradients["inputs_for_backprop"]): + self.__input_grad_cache["input_grad_%d"%(i,)] = grad + del gradients["inputs_for_backprop"] + + return gradients + + def __split_gradients_for_decoders(self, grads, decoders): + #assert self.has_multiple_decoders and self.recursive_MS_train_mode=="ALL" + num_var_per_decoder = [len(dec.trainable_variables) for dec in decoders] + if sum(num_var_per_decoder)!=len(grads): raise RuntimeError("gradients do not match variables: got {} gradients for {} variables of {} decoders {}".\ + format(len(grads), sum(num_var_per_decoder), len(decoders), num_var_per_decoder)) + split_grads = [] + pos = 0 + for size in num_var_per_decoder: + split_grads.append(grads[pos:pos+size]) + pos +=size + assert all(size==len(grad) for size, grad in zip(num_var_per_decoder, split_grads)) + return split_grads + + def set_output_gradients_for_backprop_accumulate(self, output_gradients, **kwargs): + kwargs["output_gradients"] = output_gradients + self.__output_gradient_cache = kwargs + + def backprop_accumulate(self, output_gradients, include_MS=False, include_residual=False, only_trainable=False, provide_input_gradients=False, keep_output_gradients=False): + + gradients = self.backprop(output_gradients, include_MS=include_MS, include_residual=include_residual, only_trainable=only_trainable, \ + provide_input_gradients=provide_input_gradients, keep_output_gradients=keep_output_gradients) + + with SAMPLE("NVG acc grads"): + if provide_input_gradients: + raise NotImplementedError + # can just add density variables and backprop through the whole thing, if memory allows + # currently grads there are blocked in input assembler to prevent them flowing to the volume encoder in state via the density. + input_gradients = gradients['inputs'] + + vel_net_gradients = gradients['velocity_decoder'] + if self.has_multiple_decoders: + if self.recursive_MS_train_mode=="ALL": + #var_dict = {'velocity_decoder': [var for var in dec.trainable_variables for dec in self.volume_decoder]} + # definitely not the best way of doing this... + decoders = self.active_decoders + for dec, grads in zip(decoders, self.__split_gradients_for_decoders(vel_net_gradients, decoders)): + dec.add_gradients(grads) + elif self.recursive_MS_train_mode=="TOP": + #var_dict = {'velocity_decoder': self.volume_decoder[self.recursive_MS_scales[self.recursive_MS_current_level]].trainable_variables} + self.volume_decoder[self.recursive_MS_scales[self.recursive_MS_current_level]].add_gradients(vel_net_gradients) + else: raise ValueError + else: + self.volume_decoder.add_gradients(vel_net_gradients) + + if self._has_input_encoder: + enc_net_grads = gradients["input_encoder"] + encoders = self.active_input_encoders + for enc, grads in zip(encoders, self.__split_gradients_for_decoders(enc_net_grads, encoders)): + enc.add_gradients(grads) + + if self._has_downscale_encoder: + enc_net_grads = gradients["downscale_encoder"] + encoders = self.active_downscale_encoders + for enc, grads in zip(encoders, self.__split_gradients_for_decoders(enc_net_grads, encoders)): + enc.add_gradients(grads) + + if self.requires_parent_state_variables: + state_gradients = {k: gradients[k] for k in self.parent_state.get_variables()} + #LOG.info("state grads from vel: %s"%([[tf.reduce_mean(_).numpy(), tf.reduce_max(tf.abs(_)).numpy()] if _ is not None else "None" for k,v in state_gradients.items() for _ in v])) + self.parent_state.accumulate_gradients(state_gradients) + + @property + def has_pending_gradients(self): + if self.has_multiple_decoders: + return any(_.has_pending_gradients for _ in self.volume_decoder) + else: + return self.volume_decoder.has_pending_gradients + + def apply_gradients(self, optimizer, keep_gradients=False): + raise NotImplementedError + LOG.debug("Applying velocity gradients of frame %d", self.parent_state.frame) + if self.has_multiple_decoders: + if not self.has_pending_gradients: raise RuntimeError("No decoder has any recorded gradients to apply."%(self.name,)) + for dec in self.volume_decoder: + if dec.has_pending_gradients: + dec.apply_gradients(optimizer) + if not keep_gradients: + dec.clear_gradients() + else: + self.volume_decoder.apply_gradients(optimizer) + if not keep_gradients: + self.volume_decoder.clear_gradients() + + def get_grads_vars(self, keep_gradients=True, normalize=False): + grads_vars = [] + if self.has_multiple_decoders: + for dec in self.volume_decoder: + if dec.has_pending_gradients: + grads_vars.extend(dec.get_grads_vars(keep_gradients=keep_gradients, normalize=normalize)) + if not keep_gradients: + dec.clear_gradients() + else: + grads_vars.extend(self.volume_decoder.get_grads_vars(keep_gradients=keep_gradients, normalize=normalize)) + if not keep_gradients: + self.volume_decoder.clear_gradients() + + if self._has_input_encoder: + for enc in self.active_input_encoders: + grads_vars.extend(enc.get_grads_vars(keep_gradients=keep_gradients, normalize=normalize)) + if not keep_gradients: + enc.clear_gradients() + + if self._has_downscale_encoder: + for enc in self.active_downscale_encoders: + grads_vars.extend(enc.get_grads_vars(keep_gradients=keep_gradients, normalize=normalize)) + if not keep_gradients: + enc.clear_gradients() + + return grads_vars + + def clear_gradients(self): + if self.has_multiple_decoders: + for dec in self.volume_decoder: + dec.clear_gradients() + else: + self.volume_decoder.clear_gradients() + + if self._has_input_encoder: + for enc in self.active_input_encoders: + enc.clear_gradients() + + if self._has_downscale_encoder: + for enc in self.active_downscale_encoders: + enc.clear_gradients() + + + def save(self, path): + if self.is_staggered: + np.savez_compressed(path, centered_shape=self.centered_shape, vel_x=self.x.numpy(), vel_y=self.y.numpy(), vel_z=self.z.numpy()) + elif self.is_centered: + np.savez_compressed(path, centered_shape=self.centered_shape, vel=self.centered().numpy()) + +def _tf_tensors_equal(a,b): + assert isinstance(a, (tf.Tensor, tf.Variable)), "_tf_tensors_equal input 0 is not a tf.Tensor" + assert isinstance(b, (tf.Tensor, tf.Variable)), "_tf_tensors_equal input 1 is not a tf.Tensor" + return (shape_list(a)==shape_list(b)) and tf.reduce_all(tf.equal(a,b)).numpy().tolist() + +class NeuralState(State, BackpropInterface): + def __init__(self, density, velocity, target_encoder, encoder_output_types, target_lifting, lifting_renderer, target_merging, volume_encoder, frame, prev=None, next=None, transform=None, targets=None, targets_raw=None, bkgs=None, lifting_network=None, frame_merge_network=None): + super().__init__(density=density, velocity=velocity, frame=frame, prev=prev, next=next, transform=transform, targets=targets, targets_raw=targets_raw, bkgs=bkgs) + + self.__feature_cache = ResourceCacheDictTF(self._device) + self.__input_grad_cache = ResourceCacheDictTF(device=self._device) + assert isinstance(target_lifting, str) + self.target_lifting = target_lifting + + assert is_None_or_type(target_encoder, DeferredBackpropNetwork) + self.target_encoder = target_encoder + if self.target_lifting.upper()=="UNPROJECT": + assert is_None_or_type(volume_encoder, DeferredBackpropNetwork) + self.volume_encoder = volume_encoder + else: + self.volume_encoder = None + self.encoder_output_types = encoder_output_types + if self.target_lifting.upper()=="NETWORK": + assert is_None_or_type(lifting_network, DeferredBackpropNetwork) + self.lifting_network = lifting_network + else: + self.lifting_network = None + assert is_None_or_type(frame_merge_network, DeferredBackpropNetwork) + self.frame_merge_network = frame_merge_network + + self.lifting_renderer = lifting_renderer + assert isinstance(target_merging, str) + self.target_merging = target_merging + self.clear_cache() + self.disable_cache = False #True + self.cache_warn_level = 1 #0: no warning, 1: warning, 2: error + self.input_view_mask = None + + self.__warned_shape = False + + # self.__output_users = [] + # self.__output_gradient_cache = None + # self.__inputs_for_backprop = [] + + # backprop interface + def __add_used_input(self, other, output_id): + self.__inputs_for_backprop.append(input_key(other, output_id)) + + def __register_inputs(self): + for inp, out_id in self.__inputs_for_backprop: + assert isinstance(inp, BackpropInterface) + inp._register_output_user(self, out_id) + + def __gather_inputs(self): + return [inp.output(out_id) for inp, out_id in self.__inputs_for_backprop] + + def outputs(self): + outputs = {"OUTPUT": self.targets_feature_volume()} + for cache_name, value in self.__feature_cache.items(): + if cache_name.startswith("enc_lift_volume_"): + shape = cache_name[16:] + outputs["LIFTING_"+shape] = value + + for key in outputs.keys(): + if key not in self.__output_users: + self.__output_users[key] = [] + + return outputs + + + def output(self, output_id): + outputs = self.outputs() + if output_id not in outputs: + raise KeyError("%s not in outputs. Available outputs: %s"%(output_id, tuple(outputs.keys()))) + return outputs[output_id] + + def _register_output_user(self, other, output_id): + assert isinstance(other, BackpropInterface) + if other not in self.__output_users[output_id]: + self.__output_users[output_id].append(other) + + def _compute_input_grads(self): + if self.__input_grad_cache.is_empty(): + input_grads = self.__backprop() + for i, grad in enumerate(input_grads): + self.__input_grad_cache["input_grads_%d"%(i,)] = grad + + def _get_input_grad(self, other, output_id): + # get gradients for a specific input + # check if it is one of the inputs + assert other in self.__inputs_for_backprop + t = input_key(other, output_id) + idx = self.__inputs_for_backprop.index(t) + #assert not self.__input_grad_cache.is_empty() + + self._compute_input_grads() + return self.__input_grad_cache["input_grads_%d"%(idx,)] + + def has_gradients_for(self, other, output_id): + return (input_key(other, output_id) in self.__inputs_for_backprop) and (not self.__input_grad_cache.is_empty() or self.can_backprop) + + @property + def can_backprop(self): + # has output gradients available + #LOG.info("'%s' can backprop: out grad %s; output users %d, provides grads %s", self._name, self.__output_gradient_cache is not None, len(self.__output_users), [_.has_gradients_for(self) for _ in self.__output_users]) + return (self.__output_gradient_cache is not None) or (len(self.__output_users)>0 and any(len(users)>0 and any(_.has_gradients_for(self, out_id) for _ in users) for out_id, users in self.__output_users.items())) + + @property + def requires_backprop(self): + return (not self.is_all_frame_encoders_frozen) and self.__input_grad_cache.is_empty() and self.can_backprop + + # own methods + + @property + def _device(self): + return self.density._device + + @property + def has_density_neural(self): + return self.has_density and type(self.density)==NeuralDensityGrid + + @property + def inputs_raw(self): + return self.base_targets_raw.get_images_of_views(self.input_view_mask) + @property + def inputs(self): + return self.base_targets.get_images_of_views(self.input_view_mask) + @property + def input_bkgs(self): + return self.base_bkgs.get_images_of_views(self.input_view_mask) + @property + def input_masks(self): + return self.base_masks.get_images_of_views(self.input_view_mask) + @property + def input_cameras(self): + if self.input_view_mask is not None: + return [self.base_target_cameras[_] for _ in self.input_view_mask] + else: + return copy.copy(self.base_target_cameras) + + def _get_batch_size(self): + return self.base_targets_raw.batch_size + + def _make_3D_cache_name(self, base_name, *, is_raw=False, is_binary=False, shape=None): + assert shape is None or (isinstance(shape, (list, tuple, Vector)) and len(shape) in [2,3]) + if shape is None: shape = self.transform.grid_size + return "{}{}{}_{}".format(base_name, "_raw" if is_raw else "", "_bin" if is_binary else "", "-".join(str(_) for _ in shape)) + + @property + def targets_raw_feature_volume(self): + if self.disable_cache or "target_raw_feature_volume" not in self.__feature_cache: #self.__target_raw_feature_volume is None: + # tmp = self._generate_volume_encoding(raw_targets=True) + # with tf.device(self._device): + # self.__target_raw_feature_volume = tmp + self.__feature_cache["target_raw_feature_volume"] = self._generate_volume_encoding(raw_targets=True) + else: self.__check_input_cache(raw=True) + + return self.__feature_cache["target_raw_feature_volume"] #self.__target_raw_feature_volume + #@property + def targets_feature_volume(self, grid_shape=None): + cache_name = self._make_3D_cache_name("target_feature_volume", shape=grid_shape) + if self.disable_cache or cache_name not in self.__feature_cache: #self.__target_feature_volume is None: + # tmp = self._generate_volume_encoding(raw_targets=False) + # with tf.device(self._device): + # self.__target_feature_volume = tmp + self.__feature_cache[cache_name] = self._generate_volume_encoding(raw_targets=False, grid_shape=grid_shape) + else: self.__check_input_cache(raw=False) + + return self.__feature_cache[cache_name] + + @property + def targets_raw_feature_images(self): + if self.disable_cache or "target_raw_feature_images" not in self.__feature_cache: #self.__target_raw_feature_images is None: + # self.__target_raw_feature_images = self._generate_image_encoding(raw_targets=True) + # self.__target_raw_inputs = self.inputs_raw + self.__feature_cache["target_raw_feature_images"] = self._generate_image_encoding(raw_targets=True) + self.__feature_cache["target_raw_inputs"] = self.inputs_raw + elif self.cache_warn_level>0 and "target_raw_inputs" in self.__feature_cache: #self.__target_raw_inputs is not None: + with SAMPLE("check_input_eq"): + #if not _tf_tensors_equal(self.__target_raw_inputs, self.inputs_raw): self.__cache_warning("NeuralState (%d): image encoding has not been generated or cleared for current raw targets.", self.frame) + if not _tf_tensors_equal(self.__feature_cache["target_raw_inputs"], self.inputs_raw): + self.__cache_warning("NeuralState (%d): image encoding has not been generated or cleared for current raw targets.", self.frame) + return self.__feature_cache["target_raw_feature_images"] + @property + def targets_feature_images(self): + if self.disable_cache or "target_feature_images" not in self.__feature_cache: #self.__target_feature_images is None: + # self.__target_feature_images = self._generate_image_encoding(raw_targets=False) + # self.__target_inputs = self.inputs + self.__feature_cache["target_feature_images"] = self._generate_image_encoding(raw_targets=False) + self.__feature_cache["target_inputs"] = self.inputs + elif self.cache_warn_level>0 and "target_inputs" in self.__feature_cache: #self.__target_inputs is not None: + with SAMPLE("check_input_eq"): + #if not _tf_tensors_equal(self.__target_inputs, self.inputs): self.__cache_warning("NeuralState (%d): image encoding has not been generated or cleared for current targets.", self.frame) + if not _tf_tensors_equal(self.__feature_cache["target_inputs"], self.inputs): + self.__cache_warning("NeuralState (%d): image encoding has not been generated or cleared for current targets.", self.frame) + return self.__feature_cache["target_feature_images"] #elf.__target_feature_images + + def __check_input_cache(self, raw): + assert isinstance(raw, bool) + #cache = (self.__target_raw_inputs if raw else self.__target_inputs) + cache = self.__feature_cache["target_raw_inputs" if raw else "target_inputs"] + if self.cache_warn_level>0 and cache is not None: + with SAMPLE("check_input_cache"): + if not isinstance(cache, tf.Tensor): raise TypeError("NeuralState cache is not a tf.Tensor. is: {}".format(cache.__class__.__name__)) + inputs = (self.inputs_raw if raw else self.inputs) + if not isinstance(inputs, tf.Tensor): raise TypeError("NeuralState input is not a tf.Tensor. is: {}".format(cache.__class__.__name__)) + inputs_shape = shape_list(inputs) + cache_shape = shape_list(cache) + if not len(inputs_shape)==len(cache_shape) or not all(a==b for a,b in zip(inputs_shape, cache_shape)): + self.__cache_warning("NeuralState (%d): targets and cache do not have the same shape: %s - %s. \n\ttargets: %f, %f, %f\n\tcache: %f, %f, %f", self.frame, inputs_shape, cache_shape, \ + tf.reduce_min(inputs).numpy(), tf.reduce_max(inputs).numpy(), tf.reduce_mean(inputs).numpy(), \ + tf.reduce_min(cache).numpy(), tf.reduce_max(cache).numpy(), tf.reduce_mean(cache).numpy()) + elif not tf.reduce_all(tf.equal(inputs,cache)).numpy().tolist(): + self.__cache_warning("NeuralState (%d): targets and cache do not have the same value (or contain NaN): \n\ttargets: %f, %f, %f\n\tcache: %f, %f, %f", self.frame, \ + tf.reduce_min(inputs).numpy(), tf.reduce_max(inputs).numpy(), tf.reduce_mean(inputs).numpy(), \ + tf.reduce_min(cache).numpy(), tf.reduce_max(cache).numpy(), tf.reduce_mean(cache).numpy()) + + def __cache_warning(self, *msg): + if self.cache_warn_level == 1: + LOG.warning(*msg) + elif self.cache_warn_level == 2: + LOG.error(*msg) + raise ValueError("NeuralState cache consistency error.") + + def _image_luminance(self, images): + assert has_rank(images, 5) + shape = GridShape.from_tensor(images) + if shape.c==1: return images #L + elif shape.c==2: return images[...,:1] #LA + elif shape.c==3: return tf.reduce_sum(images*self.lifting_renderer.luma, axis=-1, keepdims=True) #RGB + elif shape.c==4: return tf.reduce_sum(images[...,:3]*self.lifting_renderer.luma, axis=-1, keepdims=True) #RGBA + + + def _encode_images(self, images): + with SAMPLE("encode Images"): + assert has_rank(images, 5) + shape = GridShape.from_tensor(images) + #images = grad_log(images, "_encode_images start", LOG.info) + encoder_output = [] + + if "NETWORK" in self.encoder_output_types: + # make sure the image resolution fits the encoders strides, might get issues from distortions otherwise + # inp_div = 1 + # if isinstance(self.target_encoder, GrowingUNet): + # inp_div = self.target_encoder.get_scale() + # images = tf_pad_to_next_div_by(images, inp_div, pad_axes=(-3,-2)) + + # DEBUG + #assert self.target_encoder.current_level==0 + + shape = GridShape.from_tensor(images) + + conv_shape = [shape.n*shape.z,shape.y,shape.x,shape.c] + enc = self.target_encoder(tf.reshape(images, conv_shape)) + + enc_shape = GridShape.from_tensor(enc) + enc_shape.n = shape.n # batch + enc_shape.z = shape.z # views + enc = tf.reshape(enc, enc_shape.as_shape) + + #images = grad_log(images, "_encode_images end", LOG.info) + if not self.__warned_shape and (shape.x!=enc_shape.x or shape.y!=enc_shape.y): #check xy (spatial) match + LOG.warning("Image shape %s does not match encoded shape %s, lifting might be distorted", shape, enc_shape) + self.__warned_shape = True + + encoder_output.append(enc) + + if "L" in self.encoder_output_types: + if shape.c==1: encoder_output.append(images) #L + elif shape.c==2: encoder_output.append(images[...,:1]) #LA + elif shape.c==3: encoder_output.append(tf.reduce_sum(images*self.lifting_renderer.luma, axis=-1, keepdims=True)) #RGB + elif shape.c==4: encoder_output.append(tf.reduce_sum(images[...,:3]*self.lifting_renderer.luma, axis=-1, keepdims=True)) #RGBA + + if "IDENTITY" in self.encoder_output_types: + encoder_output.append(images) + + return tf.concat(encoder_output, axis=-1) + + def _unproject_2D(self, images_encoding, accumulate="SUM", binary=False, binary_eps_2d=1e-5, binary_eps_3d=0.5, grid_shape=None): + """ + Args: + images_encoding tf.Tensor: shape: NVHWC + returns: + tf.Tensor: features lifted to 3D, NDHWC if sum_views, VNDHWC else + """ + assert has_rank(images_encoding, 5) + assert isinstance(accumulate, str) + accumulate = accumulate.upper() + assert accumulate in ["SUM", "MEAN", "MIN", "MAX", "NONE", "CHANNELS"] + shape = GridShape.from_tensor(images_encoding) #NVHWC + + if binary: + images_encoding = tf.cast(tf.greater_equal(images_encoding, binary_eps_2d), dtype=images_encoding.dtype) + + if accumulate not in ["SUM", "MEAN"]: + images_encodings = tf.split(images_encoding, shape.z, axis=1) #V-N1WHC + shape.z = 1 + cams = [[cam] for cam in self.input_cameras] + else: + if not self.lifting_renderer.blend_mode=="ADDITIVE": + raise ValueError("SUM and MEAN unprojection merging requires ADDITIVE blend mode used in target lifting") + images_encodings = [images_encoding] #1-NVWHC + cams = [self.input_cameras] + + enc_out = [] + + transformation = self.transform.copy_no_data() + if grid_shape is not None: + transformation.grid_size = grid_shape + + for images_encoding, cameras in zip(images_encodings, cams): # for each view + with SAMPLE("unproject"): + # image shape for unprojection: NVHWC with C in [1,2,4] + if shape.c not in [1,2,4]: + channel_div = shape.c//4 if shape.c%4==0 else shape.c//2 if shape.c%2==0 else shape.c + images_encoding = tf.reshape( \ + tf.transpose( \ + tf.reshape(images_encoding, (shape.n, shape.z, shape.y, shape.x, channel_div, shape.c//channel_div)), \ + (0,4,1,2,3,5)), \ + (shape.n * channel_div, shape.z, shape.y, shape.x, shape.c//channel_div) \ + ) + #raise NotImplementedError("can only unproject with channels 1,2 or 4. is %d"%shape.c) + # roll channels into batch? + # camera XY resolution does not matter, raymarch_camera uses the input image resolution + + volume_encoding = self.lifting_renderer.raymarch_camera(data=images_encoding, cameras=cameras, transformations=transformation, inverse=True, squeeze_batch=False) + vol_shape = GridShape.from_tensor(volume_encoding) + if shape.c not in [1,2,4]: + volume_encoding = tf.reshape( \ + tf.transpose( \ + tf.reshape(volume_encoding, (shape.n, channel_div, vol_shape.z, vol_shape.y, vol_shape.x, shape.c//channel_div)), \ + (0,2,3,4,1,5)), \ + (shape.n, vol_shape.z, vol_shape.y, vol_shape.x, shape.c) \ + ) + enc_out.append(volume_encoding) + # enc_out now: V-NDHWC + + + if accumulate=="SUM": + ret = enc_out[0] + elif accumulate=="MEAN": + ret = enc_out[0] * (1.0/float(shape.z)) + elif accumulate=="MIN": + if binary: + enc_out = tf.cast(tf.greater_equal(enc_out, binary_eps_3d), dtype=images_encoding.dtype) + ret = tf.reduce_min(enc_out, axis=0) + elif accumulate=="MAX": + if binary: + enc_out = tf.cast(tf.greater_equal(enc_out, binary_eps_3d), dtype=images_encoding.dtype) + ret = tf.reduce_max(enc_out, axis=0) + elif accumulate=="CHANNELS": + ret = tf.concat(enc_out, axis=-1) + ret_shape = GridShape.from_tensor(ret) + assert ret_shape.c == shape.c*len(cams) + assert ret_shape.n == shape.n + #LOG.info("DEBUG: #target cams: %d, target mask: %s, input mask: %s."%(len(self.base_target_cameras), self.target_mask, self.input_view_mask)) + #assert ret_shape.c == 78, "debug: %s, %d"%(str(ret_shape), len(cams)) + else: + ret = enc_out + + #ret_shape = GridShape.from_tensor(ret) + #assert ret_shape.n==shape.n, "batch size mismatch after unprojection" + #assert ret_shape.c==shape.c, "channel size mismatch after unprojection" + + return ret + + def _lift_merge_encoding(self, images_encoding, grid_shape=None): + """ + Args: + images_encoding tf.Tensor: shape: NVHWC + returns: + tf.Tensor: features lifted to 3D, NDHWC + """ + assert has_rank(images_encoding, 5) + shape = GridShape.from_tensor(images_encoding) #NVHWC + #images_encoding = grad_log(images_encoding, "_lift_merge_encoding start", LOG.info) + + with SAMPLE("lift encoding"): + if self.target_lifting=="UNPROJECT": + + if self.target_merging in ["SUM","SUM_NETWORK","MEAN","MEAN_NETWORK"]: + if not self.lifting_renderer.blend_mode=="ADDITIVE": + raise ValueError("SUM and MEAN target merging requires ADDITIVE blend mode used in target lifting") + volume_encoding = self._unproject_2D(images_encoding,accumulate="SUM", grid_shape=grid_shape) + + if self.target_merging in ["MEAN","MEAN_NETWORK"]: + volume_encoding *= (1./float(shape.z)) #normalize with number of views + + if self.target_merging in ["SUM_NETWORK","MEAN_NETWORK"]: + volume_encoding = self.volume_encoder(volume_encoding) + + elif self.target_merging in ["CONCAT", "CONCAT_NETWORK"]: + # concat unprojections + volume_encoding = self._unproject_2D(images_encoding,accumulate="NONE", grid_shape=grid_shape) + with SAMPLE("Merge views"): + volume_encoding = tf.concat(volume_encoding, axis=-1) + if self.target_merging=="CONCAT_NETWORK": + with SAMPLE("Encode volume"): + volume_encoding = self.volume_encoder(volume_encoding) + elif self.target_merging=="NETWORK_CONCAT": + volume_encoding = self._unproject_2D(images_encoding,accumulate="NONE", grid_shape=grid_shape) + encodings = [] + for v in volume_encoding: + with SAMPLE("Encode volume"): + encodings.append(self.volume_encoder(v)) + #LOG.info("Volume encoder level: %d, shape %s", self.volume_encoder.get_active_level(), shape_list(encodings[-1])) + with SAMPLE("Merge views"): + volume_encoding = tf.concat(encodings, axis=-1) + elif self.target_merging=="NETWORK_SUMPROD": + # individually unproject and encode, then sum some channels and multiply others. + volume_encoding = self._unproject_2D(images_encoding,accumulate="NONE", grid_shape=grid_shape) + encodings = [] + for v in volume_encoding: + with SAMPLE("Encode volume"): + encodings.append(self.volume_encoder(v)) + + with SAMPLE("Merge views"): + #encoding_shape = shape_list(encodings[0]) + channels = self.volume_encoder.output_channels #encoding_shape[-1] + add_channels = channels//2 + #mul_channels = channels - add_channels + + add_encoding = tf.reduce_sum([_[...,:add_channels] for _ in encodings], axis=0) + mul_encoding = tf.reduce_prod([tf.tanh(_[...,add_channels:]) for _ in encodings], axis=0) + + volume_encoding = tf.concat([add_encoding, mul_encoding], axis=-1) + else: + raise ValueError("Unknown merging mode '%s'"%(self.target_merging,)) + elif self.target_lifting=="NETWORK": + assert shape.z==1, "lifting network only support single view" + # To supprt multi-view the network needs to support multi-view unprojection (and view merging) + images_encoding = tf.reshape(images_encoding, (shape.n*shape.z,shape.y,shape.x,shape.c)) + volume_encoding = self.lifting_network(images_encoding) + if self.lifting_network._enc_output: + volume_encoding, lift_encoding = volume_encoding + for level, volume in enumerate(lift_encoding): + cache_name = self._make_3D_cache_name("enc_lift_volume", shape=shape_list(volume)[1:-1]) + self.__feature_cache[cache_name] = volume + else: + raise ValueError("Unknown lifting method '%s'"%(self.target_lifting,)) + + if self.frame_merge_network is not None: + with SAMPLE("merge next frame"): + if self.next is not None: + assert isinstance(self.next, NeuralState) + #self.__inputs_for_backprop.append(self.next) + next_volume_encoding = self.next.output("OUTPUT") + self.__add_used_input(self.next, "OUTPUT") + else: + next_volume_encoding = tf.zeros_like(volume_encoding) + #DEBUG + #LOG.warning("DEBUG: merge network next frame input is zeroed!") + #next_volume_encoding = tf.zeros_like(next_volume_encoding) + volume_encoding = self.frame_merge_network(tf.concat([volume_encoding, next_volume_encoding], axis=-1)) + + self.__register_inputs() + + #volume_encoding = grad_log(volume_encoding, "_lift_merge_encoding end", LOG.info) + return volume_encoding + #return self.target_lifting(images_encoding) + + #def _merge_3D_encoding(self, lifted_encoding): + # if self.target_merging=="SUM": + # return lifted_encoding #tf.reduce_sum(lifted_encoding, axis=1) sum already done by batched unprojection + # else: + # raise ValueError("Unknown lifting method '%s'"%self.lifting) + + def _generate_image_encoding(self, raw_targets=False): + if raw_targets: raise ValueError("DEBUGGING") + images = self.inputs_raw if raw_targets else self.inputs + return self._encode_images(images) + + def _generate_volume_encoding(self, raw_targets=False, grid_shape=None): + if raw_targets: raise ValueError("DEBUGGING") + image_encoding = self.targets_raw_feature_images if raw_targets else self.targets_feature_images + + # multi-frame input + # 1. simple image concatenation + # 2. per-frame lifting, 3D concatenation + # - recursive merge instead of concat + # - alignment/warping before concat/merge + + return self._lift_merge_encoding(image_encoding, grid_shape=grid_shape) + + def _get_target_unprojection(self, is_raw, grid_shape=None): + #return self.targets_raw_feature_volume if is_raw else self.targets_feature_volume + #cache_name = "target_raw_unprojection" if is_raw else "target_unprojection" + #cache_name += "_"+str(grid_shape) + cache_name = self._make_3D_cache_name("target_unprojection", is_raw=is_raw, shape=grid_shape) + images = self.targets_raw if is_raw else self.targets + if self.disable_cache or cache_name not in self.__feature_cache: + self.__feature_cache[cache_name] = self._unproject_2D(images, accumulate="MEAN", grid_shape=grid_shape) + return self.__feature_cache[cache_name] + + def _get_input_images_unprojection(self, is_raw, grid_shape=None): + #cache_name = "input_images_raw_unprojection" if is_raw else "input_images_unprojection" + #cache_name += "_"+str(grid_shape) + cache_name = self._make_3D_cache_name("input_images_unprojection", is_raw=is_raw, shape=grid_shape) + images = self.inputs_raw if is_raw else self.inputs + if self.disable_cache or cache_name not in self.__feature_cache: + self.__feature_cache[cache_name] = self._unproject_2D(images, accumulate="MEAN", grid_shape=grid_shape) #MEAN + return self.__feature_cache[cache_name] + + def _get_input_images_unprojection_concat(self, is_raw, grid_shape=None): + #cache_name = "input_images_raw_unprojection" if is_raw else "input_images_unprojection" + #cache_name += "_"+str(grid_shape) + cache_name = self._make_3D_cache_name("input_images_unprojection_concat", is_raw=is_raw, shape=grid_shape) + images = self.inputs_raw if is_raw else self.inputs + if self.disable_cache or cache_name not in self.__feature_cache: + self.__feature_cache[cache_name] = self._unproject_2D(images, accumulate="CHANNELS", grid_shape=grid_shape) #MEAN + return self.__feature_cache[cache_name] + + def _get_target_hull(self, is_raw, binary=False, grid_shape=None): + # data = self.targets_raw_feature_images if is_raw else self.targets_feature_images + # return self._unproject_2D(data, accumulate="MIN") # NDHWC + cache_name = self._make_3D_cache_name("target_hull_volume", is_raw=is_raw, is_binary=binary, shape=grid_shape) + if self.disable_cache or cache_name not in self.__feature_cache: + if binary: + if not self.has_masks: + raise RuntimeError("No masks available to create binary target hull.") + self.__feature_cache[cache_name] = self._unproject_2D(self.masks, accumulate="MIN", binary=True, binary_eps_2d=1e-5, binary_eps_3d=0.5, grid_shape=grid_shape) + else: + data = self.targets_raw_feature_images if is_raw else self.targets_feature_images + self.__feature_cache[cache_name] = self._unproject_2D(data, accumulate="MIN", grid_shape=grid_shape) + data = self.__feature_cache[cache_name] + return data + + def _get_input_images_hull(self, is_raw, binary=False, grid_shape=None): + # data = self.targets_raw_feature_images if is_raw else self.targets_feature_images + # return self._unproject_2D(data, accumulate="MIN") # NDHWC + cache_name = self._make_3D_cache_name("input_images_hull_volume", is_raw=is_raw, is_binary=binary, shape=grid_shape) + if self.disable_cache or cache_name not in self.__feature_cache: + if binary: + if not self.has_masks: + raise RuntimeError("No masks available to create binary target hull.") + self.__feature_cache[cache_name] = self._unproject_2D(self.input_masks, accumulate="MIN", binary=True, binary_eps_2d=1e-5, binary_eps_3d=0.5, grid_shape=grid_shape) + else: + data = self.input_masks #self._image_luminance(self.inputs if is_raw else self.inputs_raw) + self.__feature_cache[cache_name] = self._unproject_2D(data, accumulate="MIN", grid_shape=grid_shape) + data = self.__feature_cache[cache_name] + return data + + def get_volume_features(self, types=[], shape=None, concat=True): + #assert "L" in self.encoder_output_types and len(self.encoder_output_types)==1, "enc types: {}".format(self.encoder_output_types) #currently using the target encoding pipeline for testing, so make sure it provides only images + data = [] + + if "INPUT_IMAGES_UNPROJECTION" in types: + data.append(self._get_input_images_unprojection(is_raw=False, grid_shape=shape)) + if "INPUT_IMAGES_UNPROJECTION_CONCAT" in types: + data.append(self._get_input_images_unprojection_concat(is_raw=False, grid_shape=shape)) + if "INPUT_IMAGES_RAW_UNPROJECTION" in types: + data.append(self._get_input_images_unprojection(is_raw=True, grid_shape=shape)) + if "INPUT_IMAGES_HULL" in types: + #if shape is not None: raise NotImplementedError + data.append(self._get_input_images_hull(is_raw=False, grid_shape=shape)) + + if "TARGET_UNPROJECTION" in types: + data.append(self._get_target_unprojection(is_raw=False, grid_shape=shape)) + if "TARGET_RAW_UNPROJECTION" in types: + data.append(self._get_target_unprojection(is_raw=True, grid_shape=shape)) + if "TARGET_HULL" in types: + data.append(self._get_target_hull(is_raw=False, grid_shape=shape)) + if "TARGET_HULL_BINARY" in types: + data.append(self._get_target_hull(is_raw=False, binary=True, grid_shape=shape)) + if "TARGET_RAW_HULL" in types: + data.append(self._get_target_hull(is_raw=True, grid_shape=shape)) + + if "ENC2D_UNPROJECTION" in types: + raise NotImplementedError + assert self.has_2D_encoder + #data.append(self.targets_feature_volume) #targets_raw_feature_volume + # if "ENC3D" in types: + # raise NotImplementedError("use state.output()") + # #if shape is not None: raise NotImplementedError + # #assert self.has_3D_encoder + # data.append(self.targets_feature_volume(grid_shape=shape)) + + # if len(data)==0: raise ValueError("No input from types: {}".format(types)) + + return tf.concat(data, axis=-1) if concat else data + + @staticmethod + def get_base_feature_channels(types=[], *, image_encoder_channels=None, volume_encoder_channels=None, color_channels=3, num_views=None): + channels = 0 + + if "INPUT_IMAGES_UNPROJECTION" in types: + channels += color_channels #1 + if "INPUT_IMAGES_UNPROJECTION_CONCAT" in types: + channels += color_channels * num_views #1 + if "INPUT_IMAGES_RAW_UNPROJECTION" in types: + channels += color_channels #1 + if "INPUT_IMAGES_HULL" in types: + channels +=1 + + if "TARGET_UNPROJECTION" in types: + channels += color_channels #1 + if "TARGET_RAW_UNPROJECTION" in types: + channels += color_channels #1 + if "TARGET_HULL" in types: + channels +=1 + if "TARGET_RAW_HULL" in types: + channels +=1 + + if "ENC2D_UNPROJECTION" in types: + channels += image_encoder_channels + if "ENC3D" in types: + channels += volume_encoder_channels + + #if channels==0: raise ValueError("No input from types: {}".format(types)) + + return channels + + + def get_variables(self): + var_dict = {} + if isinstance(self.target_encoder, (tf.keras.models.Model, DeferredBackpropNetwork)): + #LOG.debug("add encoder to NeuralState variables") + var_dict["target_encoder"] = self.target_encoder.trainable_variables + if isinstance(self.lifting_network, (tf.keras.models.Model, DeferredBackpropNetwork)): + var_dict["lifting_network"] = self.lifting_network.trainable_variables + if isinstance(self.volume_encoder, (tf.keras.models.Model, DeferredBackpropNetwork)): + #LOG.debug("add encoder to NeuralState variables") + var_dict["volume_encoder"] = self.volume_encoder.trainable_variables + if isinstance(self.frame_merge_network, (tf.keras.models.Model, DeferredBackpropNetwork)): + var_dict["frame_merge_network"] = self.frame_merge_network.trainable_variables + + return var_dict + + # def get_output_variables(self): + # pass + + def clear_cache(self, volume_only=False): + if not volume_only: + self._clear_image_cache() + self._clear_volume_cache() + + self.__output_users = {"OUTPUT": []} + self.__output_gradient_cache = None + self.__inputs_for_backprop = [] + self.__input_grad_cache.clear() + + super().clear_cache() #density, velocity and rendered images + + def _clear_image_cache(self): + self.__target_raw_inputs = None + self.__target_inputs = None + self.__target_raw_feature_images = None + self.__target_feature_images = None + + def _clear_volume_cache(self): + self.__target_raw_feature_volume = None + self.__target_feature_volume = None + self.__feature_cache.clear() + + + def clear_cache_for_backprop(self): + if "NETWORK" in self.encoder_output_types: + LOG.debug("State.clear_cache_for_backprop: clearing full cache.") + self._clear_image_cache() + self._clear_volume_cache() + elif self.target_lifting!="UNPROJECT" or ("NETWORK" in self.target_merging): + LOG.debug("State.clear_cache_for_backprop: clearing volume cache.") + self._clear_volume_cache() + + def __gather_output_grads(self): + + if self.prev is None and self.next is None: + check_output_users(self.__output_users, {"OUTPUT": [NeuralDensityGrid]}, "single frame NeuralState") + elif self.prev is None and self.next is not None: + check_output_users(self.__output_users, {"OUTPUT": [NeuralDensityGrid, NeuralVelocityGrid]}, "first frame NeuralState") + elif self.prev is not None and self.next is not None: + check_output_users(self.__output_users, {"OUTPUT": [NeuralState, NeuralVelocityGrid, NeuralVelocityGrid]}, "mid frame NeuralState") + elif self.prev is not None and self.next is None: + check_output_users(self.__output_users, {"OUTPUT": [NeuralState, NeuralVelocityGrid]}, "last frame NeuralState") + + for out_id, users in self.__output_users.items(): + for other in users: + other._compute_input_grads() + grads = {} + for out_id, users in self.__output_users.items(): + grads[out_id] = tf.zeros_like(self.output(out_id)) + for other in users: + grads[out_id] = grads[out_id] + other._get_input_grad(self, out_id) + + return grads + + def __backprop(self): + with SAMPLE("state backprop"): + output_gradients = self.__gather_output_grads() + LOG.debug("Backprop state frame %d", self.frame) + + var_dict = self.get_variables() + var_dict["inputs_for_backprop"] = self.__gather_inputs() + + self.clear_cache_for_backprop() + + with SAMPLE("forward"), tf.GradientTape(watch_accessed_variables=False) as tape: + tape.watch(var_dict) + output = self.outputs() + + + with SAMPLE("gradients"): + gradients = tape.gradient(output, var_dict, output_gradients=output_gradients) + + #if self.prev is not None: + self.accumulate_gradients(gradients) + #else: + # warnings.warn("no gradients for State of first frame.") + + return gradients["inputs_for_backprop"] + + def accumulate_gradients(self, grad_dict): + #gradients dict with keys from get_variables + if 'target_encoder' in grad_dict: + self.target_encoder.add_gradients(grad_dict["target_encoder"]) + if 'lifting_network' in grad_dict: + self.lifting_network.add_gradients(grad_dict["lifting_network"]) + if 'volume_encoder' in grad_dict: + self.volume_encoder.add_gradients(grad_dict["volume_encoder"]) + if 'frame_merge_network' in grad_dict: + self.frame_merge_network.add_gradients(grad_dict["frame_merge_network"]) + + @property + def has_pending_gradients(self): + return (isinstance(self.target_encoder, DeferredBackpropNetwork) and self.target_encoder.has_pending_gradients) \ + or (isinstance(self.volume_encoder, DeferredBackpropNetwork) and self.volume_encoder.has_pending_gradients) \ + or (isinstance(self.lifting_network, DeferredBackpropNetwork) and self.lifting_network.has_pending_gradients) \ + or (isinstance(self.frame_merge_network, DeferredBackpropNetwork) and self.frame_merge_network.has_pending_gradients) + #(isinstance(self.target_lifting, GrowingUNet) and self.target_lifting.has_pending_gradients) or (isinstance(self.target_merging, GrowingUNet) and self.target_merging.has_pending_gradients) or + + @property + def is_all_frame_encoders_frozen(self): + return not self.is_any_frame_encoders_unfrozen + @property + def is_any_frame_encoders_unfrozen(self): + return (isinstance(self.target_encoder, DeferredBackpropNetwork) and not self.target_encoder.is_frozen_weights) \ + or (isinstance(self.volume_encoder, DeferredBackpropNetwork) and not self.volume_encoder.is_frozen_weights) \ + or (isinstance(self.lifting_network, DeferredBackpropNetwork) and not self.lifting_network.is_frozen_weights) \ + or (isinstance(self.frame_merge_network, DeferredBackpropNetwork) and not self.frame_merge_network.is_frozen_weights) + + def apply_gradients(self, optimizer, apply_density_gradients=True, apply_velocity_gradients=True, keep_gradients=False): + raise NotImplementedError("Collect gradients via get_grads_vars(), call optimizer.apply_gradients() ONCE, then clear_gradients().") + if isinstance(self.target_encoder, DeferredBackpropNetwork) and self.target_encoder.has_pending_gradients: + self.target_encoder.apply_gradients(optimizer) + if not keep_gradients: + self.target_encoder.clear_gradients() + if isinstance(self.lifting_network, DeferredBackpropNetwork) and self.lifting_network.has_pending_gradients: + LOG.debug("NeuralState.apply_gradients to lifting_network.") + self.lifting_network.apply_gradients(optimizer) + if not keep_gradients: + self.lifting_network.clear_gradients() + if isinstance(self.volume_encoder, DeferredBackpropNetwork) and self.volume_encoder.has_pending_gradients: + LOG.debug("NeuralState.apply_gradients to volume_encoder.") + self.volume_encoder.apply_gradients(optimizer) + if not keep_gradients: + self.volume_encoder.clear_gradients() + if isinstance(self.frame_merge_network, DeferredBackpropNetwork) and self.frame_merge_network.has_pending_gradients: + LOG.debug("NeuralState.apply_gradients to frame_merge_network.") + self.frame_merge_network.apply_gradients(optimizer) + if not keep_gradients: + self.frame_merge_network.clear_gradients() + + if apply_velocity_gradients and isinstance(self.velocity, NeuralVelocityGrid): + self.velocity.apply_gradients(optimizer, keep_gradients=keep_gradients) + if apply_density_gradients and isinstance(self.density, NeuralDensityGrid): + self.density.apply_gradients(optimizer, keep_gradients=keep_gradients) + + def get_grads_vars(self, get_density_gradients=True, get_velocity_gradients=True, keep_gradients=True): + grads_vars = [] + if isinstance(self.target_encoder, DeferredBackpropNetwork): #and self.target_encoder.has_pending_gradients: + grads_vars.extend(self.target_encoder.get_grads_vars(keep_gradients=keep_gradients)) + if isinstance(self.lifting_network, DeferredBackpropNetwork): # and self.lifting_network.has_pending_gradients: + grads_vars.extend(self.lifting_network.get_grads_vars(keep_gradients=keep_gradients)) + if isinstance(self.volume_encoder, DeferredBackpropNetwork): # and self.volume_encoder.has_pending_gradients: + grads_vars.extend(self.volume_encoder.get_grads_vars(keep_gradients=keep_gradients)) + if isinstance(self.frame_merge_network, DeferredBackpropNetwork): # and self.frame_merge_network.has_pending_gradients: + grads_vars.extend(self.frame_merge_network.get_grads_vars(keep_gradients=keep_gradients)) + + if get_velocity_gradients and isinstance(self.velocity, NeuralVelocityGrid): + grads_vars.extend(self.velocity.get_grads_vars(keep_gradients=keep_gradients)) + if get_density_gradients and isinstance(self.density, NeuralDensityGrid): + grads_vars.extend(self.density.get_grads_vars(keep_gradients=keep_gradients)) + + return grads_vars + + def clear_gradients(self, clear_density_gradients=True, clear_velocity_gradients=True): + if isinstance(self.target_encoder, DeferredBackpropNetwork): + self.target_encoder.clear_gradients() + if isinstance(self.lifting_network, DeferredBackpropNetwork): + self.lifting_network.clear_gradients() + if isinstance(self.volume_encoder, DeferredBackpropNetwork): + self.volume_encoder.clear_gradients() + if isinstance(self.frame_merge_network, DeferredBackpropNetwork): + self.frame_merge_network.clear_gradients() + + if clear_velocity_gradients and isinstance(self.velocity, NeuralVelocityGrid): + self.velocity.clear_gradients() + if clear_density_gradients and isinstance(self.density, NeuralDensityGrid): + self.density.clear_gradients() + + + +def Sequence_set_density_for_neural_globt(self, as_var=False, device=None, dt=1.0, order=1, clamp='NONE'): + for i, state in enumerate(self): + if i>0: + #state.density = state.density.copy(as_var=as_var, device=device) + #state.density = state.density.copy_empty(as_var=as_var, device=device) + state.density = WarpedDensityGrid(order=order, dt=dt, clamp=clamp, device=device, scale_renderer=state.density.scale_renderer, is_SDF=state.density.is_SDF) + state.density.parent_state = state + +Sequence.set_density_for_neural_globt = Sequence_set_density_for_neural_globt \ No newline at end of file diff --git a/phitest/render/optimization.py b/phitest/render/optimization.py new file mode 100644 index 0000000..b6e9cdc --- /dev/null +++ b/phitest/render/optimization.py @@ -0,0 +1,2697 @@ +import sys, os +import numpy as np +import tensorflow as tf +from tensorflow.python.util import nest +from lib.util import HistoryBuffer, NO_OP, NO_CONTEXT, lerp, lerp_fast, copy_nested_structure +from lib.tf_ops import tf_None_to_const, tf_cosine_similarity, tf_pad_to_shape, shape_list, tf_laplace_filter_3d, tf_norm2 +from lib.scalar_schedule import scalar_schedule +from .renderer import RenderingContext +from .vector import GridShape +import logging, warnings + +LOG = logging.getLogger("Optimization") + +# --- LOSSES --- +class LossSchedules: + def __init__(self, *, \ + density_target=lambda i: 0.0, density_target_raw=lambda i: 0.0, \ + density_target_vol=lambda i: 0.0, density_proxy_vol=lambda i: 0.0, \ + density_target_depth_smoothness=lambda i: 0.0, \ + density_hull=lambda i: 0.0, \ + density_negative=lambda i: 0.0, density_smoothness=lambda i: 0.0, density_smoothness_2=lambda i: 0.0, \ + density_smoothness_temporal=lambda i: 0.0, density_warp=lambda i: 0.0, density_disc=lambda i: 0.0, \ + density_center=lambda i: 0.0, \ + + SDF_target_pos=lambda i: 0.0, \ + + velocity_target_vol=lambda i: 0.0, \ + velocity_warp_dens=lambda i: 0.0, velocity_warp_dens_proxy=lambda i: 0.0, velocity_warp_dens_target=lambda i: 0.0, velocity_warp_vel=lambda i: 0.0, velocity_divergence=lambda i: 0.0, \ + velocity_smoothness=lambda i: 0.0, velocity_cossim=lambda i: 0.0, velocity_magnitude=lambda i: 0.0, \ + velocity_CFLcond=lambda i: 0.0, velocity_MS_coherence=lambda i: 0.0, \ + + density_lr=lambda i: 0.0, light_lr=lambda i: 0.0, velocity_lr=lambda i: 0.0, discriminator_lr=lambda i: 0.0, \ + density_decoder_train=lambda i: True, velocity_decoder_train=lambda i: True, frame_encoders_train=lambda i: True, \ + + view_encoder_regularization=lambda i: 0.0, density_decoder_regularization=lambda i: 0.0, velocity_decoder_regularization=lambda i: 0.0, \ + discriminator_regularization=lambda i: 0.0, \ + + velocity_warp_dens_MS_weighting=lambda i: 1.0, velocity_warp_dens_proxy_MS_weighting=lambda i: 1.0, velocity_warp_dens_tar_MS_weighting=lambda i: 1.0, velocity_warp_vel_MS_weighting=lambda i: 1.0, velocity_divergence_MS_weighting=lambda i: 1.0, \ + velocity_magnitude_MS_weighting=lambda i: 1.0, \ + velocity_CFLcond_MS_weighting=lambda i: 1.0, velocity_MS_coherence_MS_weighting=lambda i: 1.0, \ + + sequence_length=lambda i: -1 \ + ): + self.density_target = density_target + self.density_target_raw = density_target_raw + self.density_target_vol = density_target_vol + self.density_proxy_vol = density_proxy_vol + self.density_target_depth_smoothness = density_target_depth_smoothness + self.density_hull = density_hull + self.density_negative = density_negative + self.density_smoothness = density_smoothness + self.density_smoothness_2 = density_smoothness_2 + self.density_smoothness_temporal = density_smoothness_temporal + self.density_warp = density_warp + self.density_disc = density_disc + self.density_center = density_center + + self.SDF_target_pos = SDF_target_pos + + self.velocity_target_vol = velocity_target_vol + self.velocity_warp_dens = velocity_warp_dens + self.velocity_warp_dens_proxy = velocity_warp_dens_proxy + self.velocity_warp_dens_target = velocity_warp_dens_target + self.velocity_warp_vel = velocity_warp_vel + self.velocity_divergence = velocity_divergence + self.velocity_smoothness = velocity_smoothness + self.velocity_cossim = velocity_cossim + self.velocity_magnitude = velocity_magnitude + self.velocity_CFLcond = velocity_CFLcond + self.velocity_MS_coherence = velocity_MS_coherence + + self.density_lr = density_lr + self.light_lr = light_lr + self.velocity_lr = velocity_lr + self.discriminator_lr = discriminator_lr + + self.density_decoder_train=density_decoder_train + self.velocity_decoder_train=velocity_decoder_train + self.frame_encoders_train=frame_encoders_train + + self.view_encoder_regularization = view_encoder_regularization + self.density_decoder_regularization = density_decoder_regularization + self.velocity_decoder_regularization = velocity_decoder_regularization + self.discriminator_regularization = discriminator_regularization + + self.velocity_warp_dens_MS_weighting = velocity_warp_dens_MS_weighting + self.velocity_warp_dens_proxy_MS_weighting = velocity_warp_dens_proxy_MS_weighting + self.velocity_warp_dens_tar_MS_weighting = velocity_warp_dens_tar_MS_weighting + self.velocity_warp_vel_MS_weighting = velocity_warp_vel_MS_weighting + self.velocity_divergence_MS_weighting = velocity_divergence_MS_weighting + self.velocity_magnitude_MS_weighting = velocity_magnitude_MS_weighting + self.velocity_CFLcond_MS_weighting = velocity_CFLcond_MS_weighting + self.velocity_MS_coherence_MS_weighting = velocity_MS_coherence_MS_weighting + + self.sequence_length = sequence_length + + def set_schedules(self, **kwargs): + for name, value in kwargs.items(): + if not hasattr(self, name): + # try: + # item = getattr(self, name) + # except (KeyError, TypeError): + raise AttributeError('Loss schedule {} does not exist.'.format(name)) + # item.set(self, value) + setattr(self, name, value) + return self + +def scale_losses(losses, scale): + if isinstance(losses, (list, tuple)): + return [_ * scale for _ in losses] + elif isinstance(losses, tf.Tensor): + return losses * scale + else: + raise TypeError + +def reduce_losses(losses): + if isinstance(losses, (list, tuple)): + return tf.reduce_sum([tf.reduce_mean(_) for _ in losses]) + elif isinstance(losses, tf.Tensor): + return tf.reduce_mean(losses) + else: + raise TypeError + +class OptimizationContext: + def __init__(self, setup, iteration, loss_schedules, \ + rendering_context, vel_scale=[1,1,1], warp_order=1, dt=1.0, buoyancy=None, \ + dens_warp_clamp="NONE", vel_warp_clamp="NONE", \ + density_optimizer=None, density_lr=1.0, light_optimizer=None, light_lr=1.0, \ + velocity_optimizer=None, velocity_lr=1.0, \ + frame=None, tf_summary=None, summary_interval=1, summary_pre=None, profiler=None, light_var_list=[], \ + allow_MS_losses=False, norm_spatial_dims=False): + self.setup = setup + self.profiler = profiler if profiler is not None else Profiler(active=False) + self.iteration = iteration + self.frame = frame + #self.loss = 0 + self._losses = {} + #self.loss_func = loss_func + self.l1_loss = lambda i, t: tf.abs(i-t) + self.l2_loss = tf.math.squared_difference #lambda i, t: (i-t)**2 + def huber_loss(i, t, delta=1.0): + #tf.losses.huber_loss does not support broadcasting... + abs_error = tf.abs(t-i) + sqr = tf.minimum(abs_error, delta) + lin = abs_error - sqr + return (0.5*(sqr*sqr)) + (lin*delta) + + self.base_loss_functions = { + # 'L0.5': lambda i, t: tf.sqrt(tf.abs(i-t)), + # 'L1': self.l1_loss, + # 'L2': self.l2_loss, + # 'L3': lambda i, t: tf.pow(tf.abs(i-t), 3), + 'RAE': lambda i, t: tf.sqrt(tf.abs(i-t)), + 'MRAE': lambda i, t: tf.reduce_mean(tf.sqrt(tf.abs(i-t))), + 'AE': lambda i, t: tf.abs(i-t), + 'SAE': lambda i, t: tf.reduce_sum(tf.abs(i-t)), + 'MAE': lambda i, t: tf.reduce_mean(tf.abs(i-t)), + 'SE': tf.math.squared_difference, + 'SSE': lambda i, t: tf.reduce_sum(tf.math.squared_difference(i,t)), + 'MSE': lambda i, t: tf.reduce_mean(tf.math.squared_difference(i,t)), + 'RMSE': lambda i, t: tf.sqrt(tf.reduce_mean(tf.math.squared_difference(i,t))), + 'CAE': lambda i, t: tf.pow(tf.abs(i-t), 3), + + 'HUBER': huber_loss, #lambda i,t: tf.losses.huber_loss(predictions=i, labels=t, reduction=tf.losses.Reduction.NONE), + #'LBE': tf_log_barrier_ext, + } + self.default_loss_function = None #self.l2_loss + self.loss_functions = { + "density/target": self.base_loss_functions["AE"], #self.l1_loss, + "density/target_raw": self.base_loss_functions["AE"], #self.l1_loss, + "density/target_pos": self.base_loss_functions["AE"], #self.l1_loss, + "density/target_vol": self.base_loss_functions["AE"], #self.l1_loss, + "density/proxy_vol": self.base_loss_functions["AE"], #self.l1_loss, + "density/target_depth_smooth": self.base_loss_functions["SE"], #self.l2_loss, + "density/hull": self.base_loss_functions["SE"], #self.l2_loss, + "density/negative": self.base_loss_functions["SE"], #self.l2_loss, + "density/edge": self.base_loss_functions["SE"], #self.l2_loss, + "density/smooth": self.base_loss_functions["SE"], #self.l2_loss, + "density/smooth-temp": self.base_loss_functions["SE"], #self.l2_loss, + "density/warp": self.base_loss_functions["AE"], #self.l1_loss, + "density/center": self.base_loss_functions["SE"], #self.l1_loss, + + "velocity/target_vol": self.base_loss_functions["AE"], #self.l1_loss, + "velocity/density_warp": self.base_loss_functions["AE"], #self.l1_loss, + "velocity/densProxy_warp": self.base_loss_functions["SE"], #self.l1_loss, + "velocity/densTar_warp": self.base_loss_functions["AE"], #self.l1_loss, + "velocity/velocity_warp": self.base_loss_functions["AE"], #self.l1_loss, + "velocity/divergence": self.base_loss_functions["SE"], #self.l2_loss, + "velocity/magnitude": self.base_loss_functions["SE"], #self.l2_loss, + "velocity/CFL": self.base_loss_functions["SE"], #self.l2_loss, + "velocity/smooth": self.base_loss_functions["SE"], + "velocity/cossim": self.base_loss_functions["SE"], + } + self.loss_schedules = loss_schedules + self.density_optimizer = density_optimizer + self.density_lr = density_lr + self.light_optimizer = light_optimizer + self.light_lr = light_lr + self.velocity_optimizer = velocity_optimizer + self.velocity_lr = velocity_lr + self._loss_summary = {} + self._tf_summary = tf_summary + self._summary_interval = summary_interval + self.summary_pre = summary_pre + self._compute_loss_summary = False + + self._target_weights = None + self._target_weights_norm = 1.0 + + self.render_ctx = rendering_context + self.render_ops = {} + self.vel_scale = vel_scale + self.buoyancy = buoyancy + self.warp_order = warp_order + self.dens_warp_clamp = dens_warp_clamp + self.vel_warp_clamp = vel_warp_clamp + self.dt = dt + self.light_var_list = light_var_list + + self.allow_MS_losses = allow_MS_losses + self.norm_spatial_dims = norm_spatial_dims + + self.warp_dens_grads = False + self.warp_dens_grads_decay = 0.9 + self.warp_vel_grads = False + self.warp_vel_grads_decay = 0.9 + self.custom_dens_grads_weight = 1.0 + self.custom_vel_grads_weight = 1.0 + + self._gradient_tape = None + + self.inspect_gradients = False + self.inspect_gradients_func = NO_OP + self.inspect_gradients_images_func = NO_OP + self.inspect_gradients_images = {} + + def start_iteration(self, iteration, force=False, compute_loss_summary=False): + '''Reset losses and set iteration + will do nothing if iteration is already set + ''' + self._compute_loss_summary = compute_loss_summary + self.set_gradient_tape() + if self.iteration==iteration and not force: + return + LOG.debug("Start iteration %d, update optimization context", iteration) + self.iteration = iteration + self._loss_summary = {} + # self.loss = 0 + self._losses = {} + + self.density_lr.assign(self.loss_schedules.density_lr(self.iteration)) + self.light_lr.assign(self.loss_schedules.light_lr(self.iteration)) + self.velocity_lr.assign(self.loss_schedules.velocity_lr(self.iteration)) + if self.record_summary: + summary_names = self.make_summary_names('density/learning_rate') + self._tf_summary.scalar(summary_names[0], self.density_lr.numpy(), step=self.iteration) + summary_names = self.make_summary_names('velocity/learning_rate') + self._tf_summary.scalar(summary_names[0], self.velocity_lr.numpy(), step=self.iteration) + + @property + def target_weights(self): + return self._target_weights + @target_weights.setter + def target_weights(self, weights): + if weights is None: + self._target_weights = None + self._target_weights_norm = 1.0 + else: + self._target_weights = tf.constant(weights, dtype=tf.float32)[:, np.newaxis, np.newaxis, np.newaxis] + self._target_weights_norm = tf.constant(1./tf.reduce_sum(self._target_weights), dtype=tf.float32) + @property + def target_weights_norm(self): + return self._target_weights_norm + + @property + def tape(self): + return self._gradient_tape + + def set_gradient_tape(self, tape=None): + self._gradient_tape = tape + + def set_loss_func(self, loss_name, loss_function): + if callable(loss_function): + self.loss_functions[loss_name] = loss_function + elif isinstance(loss_function, str): + loss_function = loss_function.upper() + if loss_function in self.base_loss_functions: + self.loss_functions[loss_name] = self.base_loss_functions[loss_function] + else: + raise ValueError("Unknown loss function {} for loss {}".format(loss_function, loss_name)) + else: + raise TypeError("Invalid loss function for loss {}".format(loss_name)) + + def get_loss_func(self, loss_name): + return self.loss_functions.get(loss_name, self.default_loss_function) + + def get_loss_func_name(self, loss_name): + func = self.get_loss_func(loss_name) + name = "UNKNOWN" + for n,f in self.base_loss_functions.items(): + if f==func: + name = n + break + return name + + + def get_losses(self): + loss_list = [] + for loss_tensors in self._losses.values(): + loss_list.extend(loss_tensors) + return loss_list + + def pop_losses(self): + '''Return current loss value and reset it to 0''' + loss = self.get_losses() + # self.loss = 0 + self._losses = {} + return loss + + def pop_loss_summary(self): + loss_summary = self._loss_summary + self._loss_summary = {} + return loss_summary + + @property + def record_summary(self): + return self._tf_summary is not None and ((self.iteration+1)%self._summary_interval)==0 + + def compute_loss_summary(self): + return (self.record_summary or self._compute_loss_summary) + + @property + def scale_density_target(self): + return self.loss_schedules.density_target(self.iteration) + + def CV(self, schedule, it=None): + '''Current Value of a scalar schedule''' + if callable(schedule): + return schedule(self.iteration if it is None else it) + else: #isinstance(schedule, collection.abs.Mapping) and ('type' in schedule): + return scalar_schedule(schedule, self.iteration if it is None else it) + + def LA(self, loss_scale): + '''Loss Active''' + if isinstance(loss_scale, bool): + return loss_scale + else: + return not np.isclose(loss_scale, 0, atol=self.setup.training.loss_active_eps) + + #def add_loss(self, loss, loss_raw=None, loss_scale=None, loss_name=None): + def add_loss(self, loss_tensors, loss_value=None, loss_value_scaled=None, loss_scale=None, loss_name=None): + '''add a loss to the accumulator and write summaries + change to: + loss_tensor, for optimization + loss_value, reduced loss_tensor, for value output + loss_scale, + ''' + #self.loss +=loss + if isinstance(loss_tensors, (list, tuple)): + self._losses[loss_name] = loss_tensors + elif isinstance(loss_tensors, tf.Tensor): + self._losses[loss_name] = [loss_tensors] + else: + raise TypeError + + if loss_name is not None: + self._loss_summary[loss_name] = (loss_value_scaled, loss_value, loss_scale) + if self.record_summary: + summary_names = self.make_summary_names(loss_name) + #self._tf_summary.scalar(summary_names[0], loss, step=self.iteration) + if loss_value_scaled is not None: + self._tf_summary.scalar(summary_names[0], loss_value_scaled, step=self.iteration) + if loss_value is not None: + self._tf_summary.scalar(summary_names[1], loss_value, step=self.iteration) + if loss_scale is not None: + self._tf_summary.scalar(summary_names[2], loss_scale, step=self.iteration) + + def get_loss(self, loss_name): + if loss_name in self._losses: + return self._losses[loss_name] + else: + raise KeyError("Loss '%s' not recorded, available losses: %s"%(loss_name, list(self._losses.keys()))) + + #def get_total_loss(self): + # return self.loss + + def add_render_op(self, name, func): + if not name in self.render_ops: self.render_ops[name] = [] + self.render_ops[name].append(func) + def remove_render_op(self, name, func): + if name in self.render_ops: + try: + i = self.render_ops[name].index(func) + except ValueError: + pass + else: + del self.render_ops[name][i] + def remove_render_ops(self, name): + if name in self.render_ops: + del self.render_ops[name] + + def RO_grid_dens_grad_scale(self, weight=1.0, sharpness=1.0, eps=1e-5): + # LOG.info("SCALE GRAD: init dens grid with weight %s", weight) + @tf.custom_gradient + def op(x): + # input: combined light-density grid NDHWC with C=4 (3 light, 1 dens) + gs = GridShape.from_tensor(x) + channel = gs.c + d = x[...,-1:] + # LOG.info("SCALE GRAD: dens grid fwd with shape %s, dens_shape %s", gs, GridShape.from_tensor(d)) + y = tf.identity(x) + def grad(dy): + # scale density gradient with exisiting density distribution + #lerp: (1-w)*dy + w*(dy*(dens/mean(dens))) + d_s = tf.pow(tf.abs(d), sharpness) + m = tf.maximum(tf.reduce_max(d_s, axis=[-4,-3,-2], keepdims=True), eps) + c = (1 - weight) + weight*(d_s/m) + if channel>1: + c = tf.pad(c, [(0,0),(0,0),(0,0),(0,0),(channel-1,0)], constant_values=1) + gs_c = GridShape.from_tensor(c) + dx = dy * c + gs_dx = GridShape.from_tensor(dx) + # LOG.info("SCALE GRAD: dens grid bwd with shape %s, c-shape %s, weight %s", gs_dx, gs_c, weight) + return dx + return y, grad + return op + def RO_frustum_dens_grad_scale(self, weight=1.0, sharpness=1.0, eps=1e-5): + #inspired by 'Single-image Tomography: 3D Volumes from 2D Cranial X-Rays' https://onlinelibrary.wiley.com/doi/abs/10.1111/cgf.13369 + @tf.custom_gradient + def op(x): + # input: sampled frustum grid NDHWC with C=4 (3 light, 1 dens) + gs = GridShape.from_tensor(x) + channel = gs.c + d = x[...,-1:] + y = tf.identity(x) + def grad(dy): + # scale density gradient with exisiting density distribution along view ray (z-axis) + #lerp: (1-w)*dy + w*(dy*(dens/mean(dens))) + d_s = tf.pow(tf.abs(d), sharpness) + m = tf.maximum(tf.reduce_max(d_s, axis=-4, keepdims=True), eps) + # dx_c = dy * tf.pad((d/m), [(0,0),(0,0),(0,0),(0,0),(3,0)], constant_values=1) + # dx = dy + weight*(dx_c - dy) + c = (1 - weight) + weight*(d_s/m) + if channel>1: + c = tf.pad(c, [(0,0),(0,0),(0,0),(0,0),(channel-1,0)], constant_values=1) + return dy * c #tf.pad((1 - weight) + weight*(d_s/m), [(0,0),(0,0),(0,0),(0,0),(3,0)], constant_values=1) + return y, grad + return op + + def set_inspect_gradient(self, active, func=None, img_func=None): + self.inspect_gradients_images = {} + if active: + self.inspect_gradients = True + self.inspect_gradients_func = func if func is not None else NO_OP + self.inspect_gradients_images_func = img_func if img_func is not None else NO_OP + else: + self.inspect_gradients = False + self.inspect_gradients_func = NO_OP + self.inspect_gradients_images_func = NO_OP + + def make_summary_names(self, loss_name): + summary_name = [] + if self.summary_pre is not None: + summary_name.append(self.summary_pre) + summary_name.append(loss_name) + summary_name.append("{type}") + if self.frame is not None: + summary_name.append('f{:04d}'.format(self.frame)) + summary_name = "/".join(summary_name) + return summary_name.format(type="final"), summary_name.format(type="raw"), summary_name.format(type="scale") + + def frame_pre(self, name): + if self.frame is not None: + return 'f{:04d}_{}'.format(self.frame, name) + return name +### Density + +def loss_dens_target(ctx, state, loss_func=None): + # Render loss for density against targets without background + loss_scale = ctx.scale_density_target + if ctx.LA(loss_scale): + use_target_hulls = False #state.density.is_SDF + if use_target_hulls: + warnings.warn("Using experimental hulls for the target loss.") + if loss_func is None: loss_func = ctx.get_loss_func("density/target") + if ctx.target_weights is None: + norm_axis = [0,1] if state.density.is_SDF else [0] + if 1 in norm_axis: warnings.warn("View norm in target loss.") + if ctx.norm_spatial_dims: norm_axis += [2,3] + elif ctx.norm_spatial_dims: + raise NotImplementedError + + tmp_loss = [] + if not (ctx.allow_MS_losses and state.density.has_MS_output): + with ctx.profiler.sample("target loss"): + if ctx.target_weights is not None: + if use_target_hulls: + tmp_loss.append(tf.reduce_sum(loss_func(state.images, state.targets)*state.masks * ctx.target_weights, axis=0) * ctx.target_weights_norm) #*state.target_hulls + else: + tmp_loss.append(tf.reduce_sum(loss_func(state.images, state.targets)*ctx.target_weights, axis=0) * ctx.target_weights_norm) #weighted mean + else: + if use_target_hulls: + tmp_loss.append(tf.reduce_mean(loss_func(state.images, state.targets)*state.masks, axis=norm_axis)) + else: + tmp_loss.append(tf.reduce_mean(loss_func(state.images, state.targets), axis=norm_axis)) #mean over batch/cameras to be independent of it + else: + with ctx.profiler.sample("target loss MS"): + for scale in state.density.gen_current_trainable_MS_scales(): + if ctx.target_weights is not None: + if use_target_hulls: + tmp_loss.append(tf.reduce_sum(loss_func(state.images_MS(scale), state.targets_MS(scale))*state.masks_MS(scale) * ctx.target_weights, axis=0) * ctx.target_weights_norm) + else: + tmp_loss.append(tf.reduce_sum(loss_func(state.images_MS(scale), state.targets_MS(scale))*ctx.target_weights, axis=0) * ctx.target_weights_norm) #weighted mean + else: + if use_target_hulls: + tmp_loss.append(tf.reduce_mean(loss_func(state.images_MS(scale), state.targets_MS(scale))*state.masks_MS(scale), axis=norm_axis)) + else: + tmp_loss.append(tf.reduce_mean(loss_func(state.images_MS(scale), state.targets_MS(scale)), axis=norm_axis)) #mean over batch/cameras to be independent of it + + tmp_loss_scaled = [_ * loss_scale for _ in tmp_loss] + if ctx.compute_loss_summary(): + ctx.add_loss(tmp_loss_scaled, tf.reduce_sum([tf.reduce_mean(_) for _ in tmp_loss]), tf.reduce_sum([tf.reduce_mean(_) for _ in tmp_loss_scaled]), loss_scale, 'density/target') + else: + ctx.add_loss(tmp_loss_scaled, None, None, loss_scale, 'density/target') + return True + return False + + +def loss_dens_target_raw(ctx, state, loss_func=None): + # Render with blended bkg loss for density against raw targets (with background) + #loss_scale = ctx.CV(ctx.setup.training.density.raw_target_loss) + loss_scale = ctx.loss_schedules.density_target_raw(ctx.iteration) + if ctx.LA(loss_scale): #and (state.targets_raw is not None or not opt_ctx.allow_free_frames) + use_target_hulls = state.density.is_SDF and not ctx.render_ctx.lights=="CAMLIGHT" #True + if use_target_hulls: + warnings.warn("Using hulls for the target_raw loss.") + if loss_func is None: loss_func = ctx.get_loss_func("density/target_raw") + loss_func_name = ctx.get_loss_func_name("density/target_raw") + warnings.warn("target_raw loss func %s"%(loss_func_name)) + if not loss_func_name=="SE": + raise ValueError("Target raw loss should be using SE!") + + if ctx.target_weights is None: + norm_axis = [0,1] if state.density.is_SDF else [0] + if 1 in norm_axis: warnings.warn("View norm in target_raw loss.") + if ctx.norm_spatial_dims: norm_axis += [2,3] + elif ctx.norm_spatial_dims: + raise NotImplementedError + + tmp_loss = [] + if not (ctx.allow_MS_losses and state.density.has_MS_output): + with ctx.profiler.sample("raw target loss"): + if ctx.target_weights is not None: + if use_target_hulls: + tmp_loss.append(tf.reduce_sum(loss_func(state.images + state.bkgs*state.t, state.targets_raw)*state.masks*ctx.target_weights, axis=0) * ctx.target_weights_norm)#*state.target_raw_hulls + else: + tmp_loss.append(tf.reduce_sum(loss_func(state.images + state.bkgs*state.t, state.targets_raw)*ctx.target_weights, axis=0) * ctx.target_weights_norm) + else: + if use_target_hulls: + tmp_loss.append(tf.reduce_mean(loss_func(state.images + state.bkgs*state.t, state.targets_raw)*state.masks, axis=norm_axis)) + else: + tmp_loss.append(tf.reduce_mean(loss_func(state.images + state.bkgs*state.t, state.targets_raw), axis=norm_axis)) + else: + with ctx.profiler.sample("raw target loss MS"): + for scale in state.density.gen_current_trainable_MS_scales(): + if ctx.target_weights is not None: + if use_target_hulls: + tmp_loss.append(tf.reduce_sum(loss_func(state.images_MS(scale) + state.bkgs_MS(scale)*state.t_MS(scale), state.targets_raw_MS(scale))*state.masks_MS(scale)*ctx.target_weights, axis=0) * ctx.target_weights_norm) + else: + tmp_loss.append(tf.reduce_sum(loss_func(state.images_MS(scale) + state.bkgs_MS(scale)*state.t_MS(scale), state.targets_raw_MS(scale))*ctx.target_weights, axis=0) * ctx.target_weights_norm) + else: + img = state.images_MS(scale) + img = img + state.bkgs_MS(scale)*state.t_MS(scale) + tar = state.targets_raw_MS(scale) + loss = loss_func(img, tar) + if use_target_hulls: + loss = loss*state.masks_MS(scale) + tmp_loss.append(tf.reduce_mean(loss, axis=norm_axis)) + else: + tmp_loss.append(tf.reduce_mean(loss, axis=norm_axis)) + + tmp_loss_scaled = [_ * loss_scale for _ in tmp_loss] + if ctx.compute_loss_summary(): + + ctx.add_loss(tmp_loss_scaled, tf.reduce_sum([tf.reduce_mean(_) for _ in tmp_loss]).numpy(), tf.reduce_sum([tf.reduce_mean(_) for _ in tmp_loss_scaled]).numpy(), loss_scale, 'density/target_raw') + else: + ctx.add_loss(tmp_loss_scaled, None, None, loss_scale, 'density/target_raw') + return True + return False + + +def loss_dens_target_vol(ctx, state, loss_func=None): + # Render with blended bkg loss for density against raw targets (with background) + loss_scale = ctx.loss_schedules.density_target_vol(ctx.iteration) + if ctx.LA(loss_scale): + if loss_func is None: loss_func = ctx.get_loss_func("density/target_vol") + + # shape: NDHWC + if ctx.target_weights is None: + norm_axis = [0] + if ctx.norm_spatial_dims: norm_axis += [1,2,3] + elif ctx.norm_spatial_dims: + raise NotImplementedError + + if not (ctx.allow_MS_losses and state.density.has_MS_output): + with ctx.profiler.sample("volume density target loss"): + tmp_loss = [] + d_curr = state.density.d + d_tar = state.density_target.d + + tmp_loss.append(tf.reduce_mean(loss_func(d_curr, d_tar), axis=norm_axis)) + else: + with ctx.profiler.sample('volume density target loss MS'): + tmp_loss = [] + for scale in state.density.gen_current_trainable_MS_scales(): #gen_current_MS_scales(): + c_shape = state.density.shape_of_scale(scale) + d_curr = state.density.d_MS(scale) + d_tar = state.density_target.scaled(c_shape) + tmp_loss.append(tf.reduce_mean(loss_func(d_curr, d_tar), axis=norm_axis)) + tmp_loss_scaled = [_ * loss_scale for _ in tmp_loss] + if ctx.compute_loss_summary(): + ctx.add_loss(tmp_loss_scaled, tf.reduce_sum([tf.reduce_mean(_) for _ in tmp_loss]), tf.reduce_sum([tf.reduce_mean(_) for _ in tmp_loss_scaled]), loss_scale, 'density/target_vol') + else: + ctx.add_loss(tmp_loss_scaled, None, None, loss_scale, 'density/target_vol') + return True + return False + +def loss_dens_proxy_vol(ctx, state, loss_func=None): + if state.has_density_neural: + return False + #loss_scale = ctx.CV(ctx.setup.training.density.raw_target_loss) + loss_scale = ctx.loss_schedules.density_proxy_vol(ctx.iteration) + if ctx.LA(loss_scale): #and (state.targets_raw is not None or not opt_ctx.allow_free_frames) + if loss_func is None: loss_func = ctx.get_loss_func("density/proxy_vol") + + # shape: NDHWC + if ctx.target_weights is None: + norm_axis = [0] + if ctx.norm_spatial_dims: norm_axis += [1,2,3] + elif ctx.norm_spatial_dims: + raise NotImplementedError + + if not (ctx.allow_MS_losses and state.density.has_MS_output): + with ctx.profiler.sample("volume density proxy loss"): + tmp_loss = [] + d_curr = state.density.d + with ctx.tape.stop_recording(): + d_tar = state.density_proxy.d + + tmp_loss.append(tf.reduce_mean(loss_func(d_curr, d_tar), axis=norm_axis)) + else: + with ctx.profiler.sample('volume density proxy loss MS'): + tmp_loss = [] + for scale in state.density.gen_current_trainable_MS_scales(): + c_shape = state.density.shape_of_scale(scale) + d_curr = state.density.d_MS(scale) + with ctx.tape.stop_recording(): + if state.density_proxy.is_MS: + d_tar = state.density_proxy.d_MS(scale) + else: + d_tar = state.density_proxy.scaled(c_shape) + tmp_loss.append(tf.reduce_mean(loss_func(d_curr, d_tar), axis=norm_axis)) + tmp_loss_scaled = [_ * loss_scale for _ in tmp_loss] + if ctx.compute_loss_summary(): + ctx.add_loss(tmp_loss_scaled, tf.reduce_sum([tf.reduce_mean(_) for _ in tmp_loss]), tf.reduce_sum([tf.reduce_mean(_) for _ in tmp_loss_scaled]), loss_scale, 'density/proxy_vol') + else: + ctx.add_loss(tmp_loss_scaled, None, None, loss_scale, 'density/proxy_vol') + return True + return False + +def loss_dens_MS_coherence(ctx, state, loss_func=None): + #use highest vel output to constrain lower vel scales + loss_scale = ctx.loss_schedules.density_MS_coherence(ctx.iteration) + top_as_label = True + + if ctx.LA(loss_scale): + if loss_func is None: loss_func = ctx.get_loss_func("density/MS_coherence") + # shape: NDHWC + if ctx.target_weights is None: + norm_axis = [0] + if ctx.norm_spatial_dims: norm_axis += [1,2,3] + elif ctx.norm_spatial_dims: + raise NotImplementedError + + if not (ctx.allow_MS_losses and state.density.has_MS_output): + LOG.warning("Loss 'density/MS_coherence' is active, but density has no MS or MS losses are not allowed.") + return False + else: + LOG.debug("Run multi-scale coherence loss for density.") + with ctx.profiler.sample('density MS coherence loss'): + tmp_loss = [] + dens_MS_scales = list(reversed(list(state.density.gen_current_MS_scales())[:-1])) #fine to coarse, starting at 2nd highest + if len(dens_MS_scales)<1: + LOG.debug("Insuficient scales for density multi-scale coherence loss.") + return False + + last_scale_dens = state.density.d_MS(state.density._get_top_active_scale()) + + for scale in dens_MS_scales: #fine to coarse, starting at 2nd highest + MS_weight = ctx.loss_schedules.density_MS_coherence_MS_weighting(scale) + # what is better, sampling always from the finest scale or only from the next finer? (regarding performance and gradient quality) + last_scale_dens = state.density.resample_density(state.density.scale_renderer, last_scale_dens, shape=state.density.shape_of_scale(scale)) + if top_as_label: + last_scale_dens = tf.stop_gradient(last_scale_dens) + + tmp_loss.append( MS_weight * tf.reduce_mean(loss_func(last_scale_dens, state.density.d_MS(scale)), axis=norm_axis)) + + tmp_loss_scaled = [_*loss_scale for _ in tmp_loss] + if ctx.compute_loss_summary(): + ctx.add_loss(tmp_loss_scaled, tf.reduce_sum([tf.reduce_mean(_) for _ in tmp_loss]), tf.reduce_sum([tf.reduce_mean(_) for _ in tmp_loss_scaled]), loss_scale, 'velocity/MS_coherence') + else: + ctx.add_loss(tmp_loss_scaled, None, None, loss_scale, 'velocity/MS_coherence') + return True + return False + +def loss_dens_target_depth_smooth(ctx, state, frustum_density, loss_func=None): + # Smoothness loss for density using forward differences (gradient computation of the 3D laplace filter convolution is so slow...) + #loss_scale = ctx.CV(ctx.setup.training.density.smoothness_loss) + loss_scale = ctx.loss_schedules.density_target_depth_smoothness(ctx.iteration) + if ctx.LA(loss_scale): + if loss_func is None: loss_func = ctx.get_loss_func("density/target_depth_smooth") + + with ctx.profiler.sample('density target depth gradient loss'): + tmp_loss = [ + loss_func(frustum_density[:,1:,:,:,:], frustum_density[:,:-1,:,:,:]), #z_grad = d[:,1:,:,:,:] - d[:,:-1,:,:,:] + ] + #tmp_loss = loss_func( (1.0/3.0)*(tf.abs(x_grad) + tf.abs(y_grad) + tf.abs(z_grad)), 0) + tmp_loss_scaled = [_ * loss_scale for _ in tmp_loss] + if ctx.compute_loss_summary(): + ctx.add_loss(tmp_loss_scaled, tf.reduce_sum([tf.reduce_mean(_) for _ in tmp_loss]), tf.reduce_sum([tf.reduce_mean(_) for _ in tmp_loss_scaled]), loss_scale, loss_name='density/target_depth_smooth') + else: + ctx.add_loss(tmp_loss_scaled, None, None, loss_scale, loss_name='density/target_depth_smooth') + return True + return False + +def loss_dens_hull(ctx, state, loss_func=None): + """ Loss to reduce density outside the hull: density*(1-hull) + This loss considers the raw density without the hull applied, even if density.restrict_to_hull is True + """ + loss_scale = ctx.loss_schedules.density_hull(ctx.iteration) + if ctx.LA(loss_scale): + raise NotImplementedError("Implement normalization") + if loss_func is None: loss_func = ctx.get_loss_func("density/hull") + with ctx.profiler.sample("density hull loss"): + tmp_loss = loss_func(state.density._d * (1.-state.density.hull), 0) + tmp_loss_scaled = tmp_loss * loss_scale + if ctx.compute_loss_summary(): + ctx.add_loss([tmp_loss_scaled], tf.reduce_mean(tmp_loss), tf.reduce_mean(tmp_loss_scaled), loss_scale, 'density/hull') + else: + ctx.add_loss([tmp_loss_scaled], None, None, loss_scale, 'density/hull') + return True + return False + +def loss_dens_negative(ctx, state, loss_func=None): + # loss for negative density + loss_scale = ctx.loss_schedules.density_negative(ctx.iteration) + if ctx.LA(loss_scale): + raise NotImplementedError("Implement normalization") + if loss_func is None: loss_func = ctx.get_loss_func("density/negative") + with ctx.profiler.sample("negative density loss"): + tmp_loss = tf.reduce_mean(loss_func(tf.maximum(-state.density._d, 0), 0), axis=0) + tmp_loss_scaled = tmp_loss * loss_scale + if ctx.compute_loss_summary(): + ctx.add_loss([tmp_loss_scaled], tf.reduce_mean(tmp_loss), tf.reduce_mean(tmp_loss_scaled), loss_scale, 'density/negative') + else: + ctx.add_loss([tmp_loss_scaled], None, None, loss_scale, 'density/negative') + return True + return False + +def get_narrow_band(data, value): + return tf.cast(tf.less_equal(tf.abs(data), value), tf.float32) + +def loss_dens_smooth(ctx, state, loss_func=None): + # Smoothness (laplace edge filter) loss for density + #loss_scale = ctx.CV(ctx.setup.training.density.smoothness_loss) + loss_scale = ctx.loss_schedules.density_smoothness(ctx.iteration) + narrow_band = 0 + if ctx.LA(loss_scale): + # shape: NDHWC + if ctx.target_weights is None: + norm_axis = [0] + if ctx.norm_spatial_dims: norm_axis += [1,2,3] + elif ctx.norm_spatial_dims: + raise NotImplementedError + + if loss_func is None: loss_func = ctx.get_loss_func("density/edge") + if not (ctx.allow_MS_losses and state.density.has_MS_output): + with ctx.profiler.sample('density edge loss'): + #tmp_loss = tf.reduce_mean(tf.abs(tf_laplace_filter_3d(state.density.d, neighbours=ctx.setup.training.density.smoothness_neighbours))) + if narrow_band>0: + d = state.density.d + with ctx.tape.stop_recording(): + mask = get_narrow_band(d, narrow_band) + tmp_loss = [tf.reduce_mean(loss_func(tf_laplace_filter_3d(d, neighbours=1, padding='VALID'), 0)*mask, axis=norm_axis)] + else: + tmp_loss = [tf.reduce_mean(loss_func(tf_laplace_filter_3d(state.density.d, neighbours=1, padding='VALID'), 0), axis=norm_axis)] #VALID=ignore borders + else: + with ctx.profiler.sample('density edge loss MS'): + tmp_loss = [] + for scale in state.density.gen_current_trainable_MS_scales(): + if narrow_band>0: + d = state.density._d_MS(scale) + with ctx.tape.stop_recording(): + mask = get_narrow_band(d, narrow_band) + tmp_loss.append(tf.reduce_mean(loss_func(tf_laplace_filter_3d(d, neighbours=1, padding='VALID'), 0)*mask, axis=norm_axis)) + else: + tmp_loss.append(tf.reduce_mean(loss_func(tf_laplace_filter_3d(state.density._d_MS(scale), neighbours=1, padding='VALID'), 0), axis=norm_axis)) #VALID=ignore borders + tmp_loss_scaled = [_ * loss_scale for _ in tmp_loss] + if ctx.compute_loss_summary(): + ctx.add_loss(tmp_loss_scaled, tf.reduce_sum([tf.reduce_mean(_) for _ in tmp_loss]), tf.reduce_sum([tf.reduce_mean(_) for _ in tmp_loss_scaled]), loss_scale, loss_name='density/edge') + else: + ctx.add_loss(tmp_loss_scaled, None, None, loss_scale, loss_name='density/edge') + return True + return False + +def loss_dens_smooth_2(ctx, state, loss_func=None): + # Smoothness loss for density using forward differences (gradient computation of the 3D laplace filter convolution is so slow...) + #loss_scale = ctx.CV(ctx.setup.training.density.smoothness_loss) + loss_scale = ctx.loss_schedules.density_smoothness_2(ctx.iteration) + if ctx.LA(loss_scale): + # shape: NDHWC + if ctx.target_weights is None: + norm_axis = [0] + if ctx.norm_spatial_dims: norm_axis += [1,2,3] + elif ctx.norm_spatial_dims: + raise NotImplementedError + + if loss_func is None: loss_func = ctx.get_loss_func("density/smooth") + if not (ctx.allow_MS_losses and state.density.has_MS_output): + with ctx.profiler.sample('density gradient loss'): + d = state.density.d + tmp_loss = [ + tf.reduce_mean(loss_func(d[:,:,:,1:,:] - d[:,:,:,:-1,:], 0), axis=norm_axis), #x_grad = d[:,:,:,1:,:] - d[:,:,:,:-1,:] + tf.reduce_mean(loss_func(d[:,:,1:,:,:] - d[:,:,:-1,:,:], 0), axis=norm_axis), #y_grad = d[:,:,1:,:,:] - d[:,:,:-1,:,:] + tf.reduce_mean(loss_func(d[:,1:,:,:,:] - d[:,:-1,:,:,:], 0), axis=norm_axis), #z_grad = d[:,1:,:,:,:] - d[:,:-1,:,:,:] + ] + #tmp_loss = loss_func( (1.0/3.0)*(tf.abs(x_grad) + tf.abs(y_grad) + tf.abs(z_grad)), 0) + else: + raise NotImplementedError + tmp_loss_scaled = [_ * loss_scale for _ in tmp_loss] + if ctx.compute_loss_summary(): + ctx.add_loss(tmp_loss_scaled, tf.reduce_sum([tf.reduce_mean(_) for _ in tmp_loss]), tf.reduce_sum([tf.reduce_mean(_) for _ in tmp_loss_scaled]), loss_scale, loss_name='density/smooth') + else: + ctx.add_loss(tmp_loss_scaled, None, None, loss_scale, loss_name='density/smooth') + return True + return False + + +def loss_dens_smooth_temporal(ctx, state, loss_func=None): + # Temoral smoothness loss for density, for tomofluid tests + #loss_scale = ctx.CV(ctx.setup.training.density.smoothness_loss) + loss_scale = ctx.loss_schedules.density_smoothness_temporal(ctx.iteration) + if ctx.LA(loss_scale) and (state.prev is not None or state.next is not None): + raise NotImplementedError("Implement normalization") + if loss_func is None: loss_func = ctx.get_loss_func("density/smooth-temp") + with ctx.profiler.sample('density temporal gradient loss'): + d = state.density.d + tmp_loss = 0 + if state.prev is not None: + tmp_loss += loss_func(state.prev.density.d, d) + if state.next is not None: + tmp_loss += loss_func(d, state.next.density.d) + tmp_loss_scaled = tmp_loss * loss_scale + if ctx.compute_loss_summary(): + ctx.add_loss([tmp_loss_scaled], tf.reduce_mean(tmp_loss), tf.reduce_mean(tmp_loss_scaled), loss_scale, 'density/smooth-temp') + else: + ctx.add_loss([tmp_loss_scaled], None, None, loss_scale, 'density/smooth-temp') + return True + return False + +def loss_dens_warp(ctx, state, loss_func=None): + #warp loss "loss(A(dt, vt), dt+1)" between prev and current and current and next state for density. + # will scale the velocity to match the density shape/resolution + #loss_scale = ctx.CV(ctx.setup.training.density.warp_loss) + loss_scale = ctx.loss_schedules.density_warp(ctx.iteration) + if ctx.LA(loss_scale) and (state.prev is not None or state.next is not None): + raise NotImplementedError("Implement normalization") + if loss_func is None: loss_func = ctx.get_loss_func("density/warp") + tmp_loss = 0 + with ctx.profiler.sample('density warp loss'): + if state.prev is not None: + tmp_loss += loss_func(state.prev.density_advected(order=ctx.warp_order, dt=ctx.dt, clamp=ctx.dens_warp_clamp), state.density.d) + if state.next is not None: + tmp_loss += loss_func(state.density_advected(order=ctx.warp_order, dt=ctx.dt, clamp=ctx.dens_warp_clamp), state.next.density.d) + tmp_loss_scaled = tmp_loss * loss_scale + if ctx.compute_loss_summary(): + ctx.add_loss([tmp_loss_scaled], tf.reduce_mean(tmp_loss), tf.reduce_mean(tmp_loss_scaled), loss_scale, 'density/warp') + else: + ctx.add_loss([tmp_loss_scaled], None, None, loss_scale, 'density/warp') + return True + return False + +#randomize_rot_cams(disc_cameras, [-30,30], [0,360]) +def loss_dens_disc(ctx, state, disc, img_list=None): + # loss from the discriminator for the density + if ctx.setup.training.discriminator.active and ctx.setup.training.discriminator.start_delay<=ctx.iteration: + #loss_scale = ctx.CV(ctx.setup.training.discriminator.loss_scale, ctx.iteration-ctx.setup.training.discriminator.start_delay) + loss_scale = ctx.loss_schedules.density_disc(ctx.iteration-ctx.setup.training.discriminator.start_delay) + loss_active = ctx.LA(loss_scale) + #randomize_rot_cams(disc_cameras, [-30,30], [0,360]) + if loss_active or disc.record_history: #only render if needed for history or loss + LOG.debug('Render discriminator input for density loss') + disc_in = disc.fake_samples(state, history_samples=False, concat=False, spatial_augment=False, name="dens_disc_samples") + # + if img_list is not None and isinstance(img_list, list): + img_list.extend(disc_in) + if loss_active: + LOG.debug('Run discriminator loss for density') + with ctx.profiler.sample("dens disc loss"): + disc_in = tf.concat(disc_in, axis=0) + if ctx.inspect_gradients: + ctx.inspect_gradients_images['density/discriminator'] = disc_in + #disc_in = disc.check_input(disc_in, "dens_disc") + disc_in = (disc_in,)if (disc.loss_type in ["SGAN"]) else (disc.real_samples(spatial_augment=True, intensity_augment=True),disc_in) + #disc_out = disc.model(disc_in, training=False) + tmp_loss, disc_scores = disc.loss(disc_in, flip_target=not (disc.loss_type in ["SGAN"]), training=False) #tf.nn.sigmoid_cross_entropy_with_logits(logits=disc_out, labels=disc.real_labels(disc_out)) + #disc.check_output(disc_out, disc_loss, disc_in, "dens_disc") + #tmp_loss = tf.math.reduce_mean(disc_loss) + tmp_loss_scaled = scale_losses(tmp_loss, loss_scale) + if ctx.compute_loss_summary(): + ctx.add_loss(tmp_loss_scaled, reduce_losses(tmp_loss), reduce_losses(tmp_loss_scaled), loss_scale, 'density/discriminator') + else: + ctx.add_loss(tmp_loss_scaled, None, None, loss_scale, 'density/discriminator') + return True + #END disc render or density loss + return False + +def get_dens_center_mask(shape, scale=1.0, squared=False): + mask = tf.abs(tf.cast(tf.linspace(1.0,-1.0, shape[0]), dtype=tf.float32)) #D + mask = mask * scale + if squared: + mask = mask*mask + mask = tf.expand_dims(mask, axis=-1) #DH + mask = tf.expand_dims(mask, axis=-1) #DHW + mask = tf.tile(mask, (1,shape[1], shape[2])) #DHW + return tf.expand_dims(mask, axis=-1) #DHWC + +def loss_dens_center(ctx, state, loss_func=None): + # push mass towards the center + loss_scale = ctx.loss_schedules.density_center(ctx.iteration) + if ctx.LA(loss_scale): + + norm_axis = [0] + if ctx.norm_spatial_dims: norm_axis += [1,2,3] + + if loss_func is None: loss_func = ctx.get_loss_func("density/center") + if not (ctx.allow_MS_losses and state.density.has_MS_output): + with ctx.profiler.sample('density center loss'): + d = state.density.d + with ctx.tape.stop_recording(): + weight = get_dens_center_mask(state.density.shape, scale=1.0, squared=True) + tmp_loss = [tf.reduce_mean(loss_func(tf.abs(d), 0)*weight, axis=norm_axis) ] + else: + with ctx.profiler.sample('density center loss MS'): + tmp_loss = [] + for scale in state.density.gen_current_trainable_MS_scales(): + d = state.density._d_MS(scale) + with ctx.tape.stop_recording(): + weight = get_dens_center_mask(state.density.shape_of_scale(scale), scale=1.0, squared=True) + tmp_loss.append(tf.reduce_mean(loss_func(tf.abs(d), 0)*weight, axis=norm_axis) ) + tmp_loss_scaled = [_ * loss_scale for _ in tmp_loss] + if ctx.compute_loss_summary(): + ctx.add_loss(tmp_loss_scaled, tf.reduce_sum([tf.reduce_mean(_) for _ in tmp_loss]), tf.reduce_sum([tf.reduce_mean(_) for _ in tmp_loss_scaled]), loss_scale, loss_name='density/center') + else: + ctx.add_loss(tmp_loss_scaled, None, None, loss_scale, loss_name='density/center') + return True + return False + +def loss_view_enc_weights(ctx, state): + loss_scale = ctx.loss_schedules.view_encoder_regularization(ctx.iteration) + if ctx.LA(loss_scale): + with ctx.profiler.sample('view encoder regularization'): + weights = state.get_variables()["encoder"] + tmp_loss = tf.reduce_mean([tf.reduce_mean(tf.nn.l2_loss(var)) for var in weights]) + tmp_loss_scaled = tmp_loss * loss_scale + ctx.add_loss([tmp_loss_scaled], tmp_loss, tmp_loss_scaled, loss_scale, 'viewenc/regularization') + return True + return False + +def loss_dens_dec_weights(ctx, state): + loss_scale = ctx.loss_schedules.density_decoder_regularization(ctx.iteration) + if ctx.LA(loss_scale): + with ctx.profiler.sample('density decoder regularization'): + weights = state.density.get_variables()["density_decoder"] + tmp_loss = tf.reduce_mean([tf.reduce_mean(tf.nn.l2_loss(var)) for var in weights]) + tmp_loss_scaled = tmp_loss * loss_scale + ctx.add_loss([tmp_loss_scaled], tmp_loss, tmp_loss_scaled, loss_scale, 'density/regularization') + return True + return False + +def warp_dens_grads(opt_ctx, state, grads, order='FWD'): + if order.upper()=='FWD': #propagate density gradients to next state, simple forward warp. do not clamp negative gradients + raise NotImplementedError + return state.prev.velocity.warp(grads, order=opt_ctx.warp_order, dt=opt_ctx.dt, clamp="NONE" if opt_ctx.dens_warp_clamp=="NEGATIVE" else opt_ctx.dens_warp_clamp), tf.constant(0, dtype=tf.float32), tf.constant(0, dtype=tf.float32), tf.constant(0, dtype=tf.float32) + elif order.upper()=='BWD': #propagate density gradients to previous state, backprop through prev->warp + #d = state.density.with_inflow() + var_list = state.get_output_variables() #state.density.var_list() + state.velocity.var_list() #state.var_list() + with tf.GradientTape(watch_accessed_variables=False) as tape: + tape.watch(var_list) + d_warp = state.density_advected(order=opt_ctx.warp_order, dt=opt_ctx.dt, clamp=opt_ctx.dens_warp_clamp)#state.velocity.warp(d, order=opt_ctx.warp_order, dt=opt_ctx.dt) + return tape.gradient([d_warp], var_list, output_gradients=[grads]) + else: + raise ValueError + +def apply_custom_grads_with_check(grad, custom_grad, custom_grad_scale=1.): + if grad is None and custom_grad is None: + LOG.warning("apply_custom_grads: base and custom gradient is None.") + + if grad is None: + #LOG.warning("apply_custom_grads: base gradient is None.") + grad = 0. + if custom_grad is None: + #LOG.warning("apply_custom_grads: custom gradient is None.") + custom_grad = 0. + + return grad + custom_grad * custom_grad_scale + +def optStep_density(opt_ctx, state, use_vel=False, disc_ctx=None, disc_samples_list=None, custom_dens_grads=None, apply_dens_grads=False): + + #dens_var_list = state.density.var_list() + dens_vars = state.density.get_output_variables(include_MS=opt_ctx.allow_MS_losses, include_residual=True, only_trainable=True) # get_variables() + with opt_ctx.profiler.sample('optStep_density'): + #with opt_ctx.profiler.sample('loss'), tf.GradientTape(watch_accessed_variables=False, persistent=opt_ctx.inspect_gradients) as dens_tape: + with opt_ctx.profiler.sample('loss'), tf.GradientTape(watch_accessed_variables=False, persistent=True) as dens_tape: + dens_tape.watch(dens_vars) + if opt_ctx.light_var_list: + dens_tape.watch(opt_ctx.light_var_list) + if opt_ctx.inspect_gradients: + dens_inspect_vars = {} + dens_inspect_vars.update(dens_vars) + if opt_ctx.light_var_list: dens_inspect_vars['lights'] = opt_ctx.light_var_list + opt_ctx.set_gradient_tape(dens_tape) + + catch_frustum_grid = opt_ctx.LA(opt_ctx.loss_schedules.density_target_depth_smoothness(opt_ctx.iteration)) + fg_container = [] + if catch_frustum_grid: + # use the custom render op hooks to catch the reference to the frutum grid tensor + def _catch_FG(fg): + fg_container.append(fg) + return fg + opt_ctx.add_render_op("FRUSTUM", _catch_FG) + else: + fg_container.append(None) + + + state.render_density(opt_ctx.render_ctx, custom_ops=opt_ctx.render_ops) + if opt_ctx.allow_MS_losses and state.density.is_MS: + state.render_density_MS_stack(opt_ctx.render_ctx, custom_ops=opt_ctx.render_ops) + + if catch_frustum_grid: + opt_ctx.remove_render_op("FRUSTUM", _catch_FG) + + LOG.debug("Density losses") + active_density_loss = False + # Render loss for density against targets without background + active_density_loss = loss_dens_target(opt_ctx, state) or active_density_loss + active_density_loss = loss_dens_target_raw(opt_ctx, state) or active_density_loss + active_density_loss = loss_dens_target_vol(opt_ctx, state) or active_density_loss + active_density_loss = loss_dens_proxy_vol(opt_ctx, state) or active_density_loss + active_density_loss = loss_dens_target_depth_smooth(opt_ctx, state, fg_container[0]) or active_density_loss + active_density_loss = loss_dens_hull(opt_ctx, state) or active_density_loss + active_density_loss = loss_dens_negative(opt_ctx, state) or active_density_loss + active_density_loss = loss_dens_smooth(opt_ctx, state) or active_density_loss + active_density_loss = loss_dens_smooth_2(opt_ctx, state) or active_density_loss + active_density_loss = loss_dens_smooth_temporal(opt_ctx, state) or active_density_loss + + if use_vel and state.velocity is not None: + active_density_loss = loss_dens_warp(opt_ctx, state) or active_density_loss + + if disc_ctx is not None: + active_density_loss = loss_dens_disc(opt_ctx, state, disc_ctx, disc_samples_list) or active_density_loss + + #density_loss = opt_ctx.loss #opt_ctx.pop_loss() + #END gradient tape + if active_density_loss: + if custom_dens_grads is not None: + cdg_scale = opt_ctx.CV(opt_ctx.custom_dens_grads_weight) + with opt_ctx.profiler.sample('gradient'): + if opt_ctx.inspect_gradients: + for loss_name in opt_ctx._losses: + dens_grads = dens_tape.gradient(opt_ctx.get_loss(loss_name), dens_inspect_vars) + for k, g in dens_grads.items(): + #LOG.info("inspect gradient %s", k) + if k.startswith('density'): + if g is not None: + opt_ctx.inspect_gradients_func(opt_ctx=opt_ctx, gradients=g, name=loss_name+k[7:]) + #else: + #LOG.info("gradient %s is None", k) + del dens_grads + + if loss_name in opt_ctx.inspect_gradients_images: + img_grads = dens_tape.gradient(opt_ctx.get_loss(loss_name), [opt_ctx.inspect_gradients_images[loss_name]]) + opt_ctx.inspect_gradients_images_func(opt_ctx=opt_ctx, gradients=img_grads[0], name=loss_name) + del img_grads + del opt_ctx.inspect_gradients_images[loss_name] + + if custom_dens_grads is not None and opt_ctx.LA(cdg_scale): + + has_valid_dens_grads = False + for k, g in custom_dens_grads.items(): + if k.startswith('density'): + if g is not None: + has_valid_dens_grads = True + opt_ctx.inspect_gradients_func(opt_ctx=opt_ctx, gradients=g, name="density/custom_grad"+k[7:]) + else: + LOG.debug("Custom density gradient of frame %d for '%s' is None.", state.frame, k) + if not has_valid_dens_grads: + LOG.warning("All custom density gradients of frame %d are None.", state.frame) + + if custom_dens_grads.get('inflow') is not None: + opt_ctx.inspect_gradients_func(opt_ctx=opt_ctx, gradients=custom_dens_grads['inflow']*cdg_scale, name="custom_inflow_grad") + opt_ctx.inspect_gradients_images = {} + # + #if opt_ctx.inspect_gradients: + dens_grads = dens_tape.gradient(opt_ctx.get_losses(), dens_inspect_vars) + has_valid_dens_grads = False + for k, g in dens_grads.items(): + if k.startswith('density'): + if g is not None: + has_valid_dens_grads = True + opt_ctx.inspect_gradients_func(opt_ctx=opt_ctx, gradients=g, name="density/_local"+k[7:]) + else: + LOG.debug("Total density of frame %d for '%s' is None.", state.frame, k) + if not has_valid_dens_grads: + LOG.warning("All local density gradients of frame %d are None.", state.frame) + + if dens_grads.get('inflow') is not None: #len(dens_grads)>1 and dens_grads[1] is not None: + opt_ctx.inspect_gradients_func(opt_ctx=opt_ctx, gradients=dens_grads['inflow'], name="density/inflow") + if dens_grads.get('lights') is not None: + for i, lg in enumerate(dens_grads['lights']): + opt_ctx.inspect_gradients_func(opt_ctx=opt_ctx, gradients=lg, name="density/light_{}".format(i)) + + del dens_grads + + LOG.debug('Compute and apply density gradients') + if opt_ctx.light_var_list: + dens_vars['lights'] = opt_ctx.light_var_list + dens_grads = dens_tape.gradient(opt_ctx.get_losses(), dens_vars) + del dens_tape + + curr_dens_grads = copy_nested_structure(dens_grads) + if custom_dens_grads is not None and opt_ctx.LA(cdg_scale): + if opt_ctx.allow_MS_losses and state.density.has_MS_output: + # back-warping give gradients w.r.t the the output variables, not the MS-output variables, so map to the highest MS-output. + custom_dens_grads = state.density.map_gradient_output_to_MS(custom_dens_grads) + #dens_grads = nest.map_structure(lambda d, c: apply_custom_grads_with_check(d, c, cdg_scale), dens_grads, custom_dens_grads) + + for k in dens_grads: #depth 1 sufficient for now... + if k in custom_dens_grads: + if custom_dens_grads[k] is not None: + LOG.debug("Update density gradient '%s' with custom gradient.", k) + if dens_grads[k] is None: + #LOG.warning("This should crash now...") + dens_grads[k] = custom_dens_grads[k]*cdg_scale + else: + dens_grads[k] += custom_dens_grads[k]*cdg_scale + #backprop_accumulate hadles this already + # elif dens_grads[k] is None: + # dens_grads[k] = 0.0 + #else: + # LOG.debug("Custom density gradient '%s' of frame %d is None", k, state.frame) + + for k in custom_dens_grads: + if k not in dens_grads: + LOG.warning("Custom density gradient '%s' can't be mapped and will be ignored.", k) + + if opt_ctx.light_var_list: + opt_ctx.light_optimizer.apply_gradients(zip(dens_grads['lights'], opt_ctx.light_var_list)) + del dens_grads['lights'] + del dens_vars['lights'] + + if "inflow" in dens_grads: + opt_ctx.density_optimizer.apply_gradients(zip(dens_grads["inflow"], dens_vars["inflow"])) + del dens_grads['inflow'] + del dens_vars['inflow'] + + #LOG.debug("Apply dens grads of frame %d in iteration %d: %s", state.frame, opt_ctx.iteration, apply_dens_grads) + state.density.set_output_gradients_for_backprop_accumulate(dens_grads, include_MS=opt_ctx.allow_MS_losses, include_residual=True, only_trainable=True) + if apply_dens_grads: + #state.density.backprop_accumulate(dens_grads, include_MS=opt_ctx.allow_MS_losses, include_residual=True, only_trainable=True) + state.density._compute_input_grads() # calls backprop_accumulate with grads set before. + + else: + curr_dens_grads = nest.map_structure(lambda v: tf.constant(0, dtype=tf.float32), dens_vars) #[tf.constant(0, dtype=tf.float32) for _ in range(len(dens_var_list))] + + opt_ctx.set_gradient_tape() + #del dens_tape + + with opt_ctx.profiler.sample('clamp density'): #necessary as negative density really breaks the rendering + d = state.density.d + # if opt_ctx.setup.training.density.use_hull: + # d = d*state.hull # hull is a binary mask + #d = tf.clip_by_value(d, opt_ctx.CV(opt_ctx.setup.data.density.min), opt_ctx.CV(opt_ctx.setup.data.density.max)) + #state.density.assign(d) + state.density.apply_clamp(opt_ctx.CV(opt_ctx.setup.data.density.min), opt_ctx.CV(opt_ctx.setup.data.density.max)) + + return active_density_loss, curr_dens_grads + +### Velocity + +def loss_vel_target_vol(ctx, state, loss_func=None): + # Render with blended bkg loss for density against raw targets (with background) + #loss_scale = ctx.CV(ctx.setup.training.density.raw_target_loss) + loss_scale = ctx.loss_schedules.velocity_target_vol(ctx.iteration) + if ctx.LA(loss_scale): #and (state.targets_raw is not None or not opt_ctx.allow_free_frames) + if loss_func is None: loss_func = ctx.get_loss_func("velocity/target_vol") + # shape: NDHWC + if ctx.target_weights is None: + norm_axis = [0] + if ctx.norm_spatial_dims: norm_axis += [1,2,3] + elif ctx.norm_spatial_dims: + raise NotImplementedError + + if not (ctx.allow_MS_losses and state.velocity.has_MS_output): + with ctx.profiler.sample("volume velocity target loss"): + if state.velocity.is_centered: + tmp_loss = [ + tf.reduce_mean(loss_func(state.velocity.centered(), state.velocity_target.centered()), axis=norm_axis), + ] + elif state.velocity.is_staggered: + tmp_loss = [ + tf.reduce_mean(loss_func(state.velocity.x, state.velocity_target.x), axis=norm_axis), + tf.reduce_mean(loss_func(state.velocity.y, state.velocity_target.y), axis=norm_axis), + tf.reduce_mean(loss_func(state.velocity.z, state.velocity_target.z), axis=norm_axis), + ] + else: + raise ValueError("Unknown velocity type.") + else: + with ctx.profiler.sample("volume velocity target MS loss"): + tmp_loss=[] + if state.velocity.is_centered: + + for scale in state.velocity.gen_current_trainable_MS_scales(): + #MS_weight = ctx.loss_schedules.velocity_target_vol_MS_weighting(scale) + # what is better, sampling always from the finest scale or only from the next finer? (regarding performance and gradient quality) + vel_target = state.velocity.resample_velocity(state.velocity.scale_renderer, state.velocity_target.centered(), shape=state.velocity.centered_shape_of_scale(scale), \ + is_staggered=state.velocity.is_staggered, scale_magnitude=True) + + tmp_loss.append( tf.reduce_mean(loss_func(vel_target, state.velocity.centered_MS(scale)), axis=norm_axis)) + + elif state.velocity.is_staggered: + #last_scale_vel = state.velocity._staggered_MS(state.velocity._get_top_active_scale()) # (x,y,z) + + for scale in state.velocity.gen_current_trainable_MS_scales(): + #MS_weight = ctx.loss_schedules.velocity_target_vol_MS_weighting(scale) + # what is better, sampling always from the finest scale or only from the next finer? (regarding performance and gradient quality) + vel_target = state.velocity.resample_velocity(state.velocity.scale_renderer, state.velocity_target._staggered(), shape=state.velocity.centered_shape_of_scale(scale), \ + is_staggered=state.velocity.is_staggered, scale_magnitude=True) + + tmp_loss.append( tf.reduce_mean(loss_func(vel_target[0], state.velocity._staggered_MS(scale)[0]), axis=norm_axis)) #x + tmp_loss.append( tf.reduce_mean(loss_func(vel_target[1], state.velocity._staggered_MS(scale)[1]), axis=norm_axis)) #y + tmp_loss.append( tf.reduce_mean(loss_func(vel_target[2], state.velocity._staggered_MS(scale)[2]), axis=norm_axis)) #z + else: + raise ValueError("Unknown velocity type.") + tmp_loss_scaled = [_ * loss_scale for _ in tmp_loss] + if ctx.compute_loss_summary(): + ctx.add_loss(tmp_loss_scaled, tf.reduce_sum([tf.reduce_mean(_) for _ in tmp_loss]), tf.reduce_sum([tf.reduce_mean(_) for _ in tmp_loss_scaled]), loss_scale, 'velocity/target_vol') + else: + ctx.add_loss(tmp_loss_scaled, None, None, loss_scale, 'velocity/target_vol') + return True + return False + +def loss_vel_warp_dens(ctx, state, loss_func=None): + #warp loss "loss(A(dt, vt), dt+1)" between current and next state for velocity. + #loss_scale = ctx.CV(ctx.setup.training.velocity.density_warp_loss) + loss_scale = ctx.loss_schedules.velocity_warp_dens(ctx.iteration) + if ctx.LA(loss_scale) and state.next is not None: + if loss_func is None: loss_func = ctx.get_loss_func("velocity/density_warp") + # shape: NDHWC + if ctx.target_weights is None: + norm_axis = [0] + if ctx.norm_spatial_dims: norm_axis += [1,2,3] + elif ctx.norm_spatial_dims: + raise NotImplementedError + + if not (ctx.allow_MS_losses and state.velocity.has_MS_output): + LOG.debug("Run dens-warp loss for velocity, curr to next") + with ctx.profiler.sample('velocity dens warp loss'): + curr_dens = state.density.scaled(state.velocity.centered_shape, with_inflow=True) + next_dens = state.next.density.scaled(state.velocity.centered_shape) + tmp_loss = [tf.reduce_mean(loss_func(state.velocity.warp(curr_dens, order=ctx.warp_order, dt=ctx.dt, clamp=ctx.dens_warp_clamp), next_dens), axis=norm_axis)] + else: + LOG.debug("Run multi-scale dens-warp loss for velocity, curr to next") + with ctx.profiler.sample('velocity dens warp loss MS'): + tmp_loss = [] + for scale in state.velocity.gen_current_trainable_MS_scales(): #gen_current_MS_scales(): + c_shape = state.velocity.centered_shape_of_scale(scale) + curr_dens = state.density.scaled(c_shape, with_inflow=True) + next_dens = state.next.density.scaled(c_shape) + c_vel = state.velocity.centered_MS(scale) + MS_weight = ctx.loss_schedules.velocity_warp_dens_MS_weighting(scale) + tmp_loss.append( tf.reduce_mean(loss_func(state.velocity.warp(curr_dens, order=ctx.warp_order, dt=ctx.dt, clamp=ctx.dens_warp_clamp, centered_velocity=c_vel), next_dens), axis=norm_axis) * MS_weight) + tmp_loss_scaled = [_*loss_scale for _ in tmp_loss] + if ctx.compute_loss_summary(): + ctx.add_loss(tmp_loss_scaled, tf.reduce_sum([tf.reduce_mean(_) for _ in tmp_loss]), tf.reduce_sum([tf.reduce_mean(_) for _ in tmp_loss_scaled]), loss_scale, 'velocity/density_warp') + else: + ctx.add_loss(tmp_loss_scaled, None, None, loss_scale, 'velocity/density_warp') + return True + return False + +def loss_vel_warp_dens_proxy(ctx, state, loss_func=None): + #warp loss "loss(A(dt, vt), dt+1)" between current and next state for velocity. + #loss_scale = ctx.CV(ctx.setup.training.velocity.density_warp_loss) + loss_scale = ctx.loss_schedules.velocity_warp_dens_proxy(ctx.iteration) + if ctx.LA(loss_scale) and state.next is not None: + if not state.next.has_density_proxy: + raise RuntimeError("proxy warp loss target state has no density proxy.") + if loss_func is None: loss_func = ctx.get_loss_func("velocity/densProxy_warp") + # shape: NDHWC + if ctx.target_weights is None: + norm_axis = [0] + if ctx.norm_spatial_dims: norm_axis += [1,2,3] + elif ctx.norm_spatial_dims: + raise NotImplementedError + + if not (ctx.allow_MS_losses and state.velocity.has_MS_output): + LOG.debug("Run dens-proxy-warp loss for velocity, curr to next") + with ctx.profiler.sample('velocity dens-proxy warp loss'): + curr_dens = state.density.scaled(state.velocity.centered_shape, with_inflow=True) + with ctx.tape.stop_recording(): + next_dens = state.next.density_proxy.scaled(state.velocity.centered_shape) + #next_dens = tf.stop_gradient(next_dens) + tmp_loss = [tf.reduce_mean(loss_func(state.velocity.warp(curr_dens, order=ctx.warp_order, dt=ctx.dt, clamp=ctx.dens_warp_clamp), next_dens), axis=norm_axis)] + else: + LOG.debug("Run multi-scale dens-proxy-warp loss for velocity, curr to next") + with ctx.profiler.sample('velocity dens-proxy warp loss MS'): + tmp_loss = [] + for scale in state.velocity.gen_current_trainable_MS_scales(): #gen_current_MS_scales(): + c_shape = state.velocity.centered_shape_of_scale(scale) + curr_dens = state.density.scaled(c_shape, with_inflow=True) + with ctx.tape.stop_recording(): + next_dens = state.next.density_proxy.scaled(c_shape) + #next_dens = tf.stop_gradient(next_dens) + c_vel = state.velocity.centered_MS(scale) + MS_weight = ctx.loss_schedules.velocity_warp_dens_proxy_MS_weighting(scale) + tmp_loss.append( tf.reduce_mean(loss_func(state.velocity.warp(curr_dens, order=ctx.warp_order, dt=ctx.dt, clamp=ctx.dens_warp_clamp, centered_velocity=c_vel), next_dens), axis=norm_axis) * MS_weight) + tmp_loss_scaled = [_*loss_scale for _ in tmp_loss] + if ctx.compute_loss_summary(): + ctx.add_loss(tmp_loss_scaled, tf.reduce_sum([tf.reduce_mean(_) for _ in tmp_loss]), tf.reduce_sum([tf.reduce_mean(_) for _ in tmp_loss_scaled]), loss_scale, 'velocity/densProxy_warp') + else: + ctx.add_loss(tmp_loss_scaled, None, None, loss_scale, 'velocity/densProxy_warp') + return True + return False + +def loss_vel_warp_dens_target(ctx, state, loss_func=None): + #warp loss "loss(A(dt, vt), dt+1)" between current and next state for velocity. + #loss_scale = ctx.CV(ctx.setup.training.velocity.density_warp_loss) + loss_scale = ctx.loss_schedules.velocity_warp_dens_target(ctx.iteration) + if ctx.LA(loss_scale) and state.next is not None: + if loss_func is None: loss_func = ctx.get_loss_func("velocity/densTar_warp") + # shape: NDHWC + if ctx.target_weights is None: + norm_axis = [0] + if ctx.norm_spatial_dims: norm_axis += [1,2,3] + elif ctx.norm_spatial_dims: + raise NotImplementedError + + if not (ctx.allow_MS_losses and state.velocity.has_MS_output): + LOG.debug("Run dens-tar-warp loss for velocity, curr to next") + with ctx.profiler.sample('velocity dens-target warp loss'): + curr_dens = state.density.scaled(state.velocity.centered_shape, with_inflow=True) + next_dens = state.next.density_target.scaled(state.velocity.centered_shape) + tmp_loss = [tf.reduce_mean(loss_func(state.velocity.warp(curr_dens, order=ctx.warp_order, dt=ctx.dt, clamp=ctx.dens_warp_clamp), next_dens), axis=norm_axis)] + else: + LOG.debug("Run multi-scale dens-tar-warp loss for velocity, curr to next") + with ctx.profiler.sample('velocity dens-target warp loss MS'): + tmp_loss = [] + for scale in state.velocity.gen_current_trainable_MS_scales(): #gen_current_MS_scales(): + c_shape = state.velocity.centered_shape_of_scale(scale) + curr_dens = state.density.scaled(c_shape, with_inflow=True) + next_dens = state.next.density_target.scaled(c_shape) + c_vel = state.velocity.centered_MS(scale) + MS_weight = ctx.loss_schedules.velocity_warp_dens_tar_MS_weighting(scale) + tmp_loss.append( tf.reduce_mean(loss_func(state.velocity.warp(curr_dens, order=ctx.warp_order, dt=ctx.dt, clamp=ctx.dens_warp_clamp, centered_velocity=c_vel), next_dens), axis=norm_axis) * MS_weight) + tmp_loss_scaled = [_*loss_scale for _ in tmp_loss] + if ctx.compute_loss_summary(): + ctx.add_loss(tmp_loss_scaled, tf.reduce_sum([tf.reduce_mean(_) for _ in tmp_loss]), tf.reduce_sum([tf.reduce_mean(_) for _ in tmp_loss_scaled]), loss_scale, 'velocity/densTar_warp') + else: + ctx.add_loss(tmp_loss_scaled, None, None, loss_scale, 'velocity/densTar_warp') + return True + return False + +def loss_vel_warp_vel(ctx, state, loss_func=None): + #warp loss "loss(A(vt, vt), vt+1)" between prev and current and current and next state for velocity. + #loss_scale = ctx.CV(ctx.setup.training.velocity.velocity_warp_loss) + loss_scale = ctx.loss_schedules.velocity_warp_vel(ctx.iteration) + if ctx.LA(loss_scale) and (state.prev is not None or state.next is not None): + raise NotImplementedError("Implement normalization") + if loss_func is None: loss_func = ctx.get_loss_func("velocity/velocity_warp") + LOG.debug("Run vel-warp loss for velocity") + tmp_loss = [0,0,0] + with ctx.profiler.sample('velocity vel warp loss'): + if state.prev is not None:# and state.next is not None: + if (ctx.allow_MS_losses and state.velocity.has_MS_output and state.prev.velocity.has_MS_output): + if not state.velocity.has_same_MS_shapes(state.prev.velocity): + raise RuntimeError("multi-scale shapes of velocities of frames %d and %d are not compatible."%(state.frame, state.next.frame)) + raise NotImplementedError("MS loss for vel warp vel loss not implemented.") + LOG.debug("Warp loss prev to curr") + prev_warped = state.prev.velocity_advected(order=ctx.warp_order, dt=ctx.dt, clamp=ctx.vel_warp_clamp) + + # buoyancy + if ctx.setup.training.optimize_buoyancy or tf.reduce_any(tf.not_equal(ctx.buoyancy, 0.0)): + prev_warped = prev_warped.with_buoyancy(ctx.buoyancy, state.density) + + tmp_loss[0] += loss_func(prev_warped.x, state.velocity.x) + tmp_loss[1] += loss_func(prev_warped.y, state.velocity.y) + tmp_loss[2] += loss_func(prev_warped.z, state.velocity.z) + if state.next is not None:# and state.next.next is not None: + if (ctx.allow_MS_losses and state.velocity.has_MS_output and state.next.velocity.has_MS_output): + if not state.velocity.has_same_MS_shapes(state.next.velocity): + raise RuntimeError("multi-scale shapes of velocities of frames %d and %d are not compatible."%(state.frame, state.next.frame)) + raise NotImplementedError("MS loss for vel warp vel loss not implemented.") + LOG.debug("Warp loss curr to next") + curr_warped = state.velocity_advected(order=ctx.warp_order, dt=ctx.dt, clamp=ctx.vel_warp_clamp) + + # buoyancy + if ctx.setup.training.optimize_buoyancy or tf.reduce_any(tf.not_equal(ctx.buoyancy, 0.0)): + curr_warped = curr_warped.with_buoyancy(ctx.buoyancy, state.next.density) + + tmp_loss[0] += loss_func(curr_warped.x, state.next.velocity.x) + tmp_loss[1] += loss_func(curr_warped.y, state.next.velocity.y) + tmp_loss[2] += loss_func(curr_warped.z, state.next.velocity.z) + tmp_loss_scaled = [_ * loss_scale for _ in tmp_loss] + if ctx.compute_loss_summary(): + ctx.add_loss(tmp_loss_scaled, tf.reduce_sum([tf.reduce_mean(_) for _ in tmp_loss]), tf.reduce_sum([tf.reduce_mean(_) for _ in tmp_loss_scaled]), loss_scale, 'velocity/velocity_warp') + else: + ctx.add_loss(tmp_loss_scaled, None, None, loss_scale, 'velocity/velocity_warp') + return True + return False + +''' +def loss_vel_smooth(ctx, state): + ''Smoothness (laplace edge filter) loss for velocity'' + #loss_scale = ctx.CV(ctx.setup.training.velocity.smoothness_loss) + loss_scale = ctx.loss_schedules.velocity_smoothness(ctx.iteration) + if ctx.LA(loss_scale): + raise NotImplementedError + LOG.debug("Run smoothness loss for velocity") + with ctx.profiler.sample('velocity edge loss'): + vel_components = state.velocity.var_list() + #normalize to unit width to prevent loss scaling issues with growing optimization + vel_components = [component*scale for component, scale in zip(vel_components, ctx.vel_scale[::-1])] + tmp_loss = tf.reduce_mean([tf.reduce_mean(tf.abs(tf_laplace_filter_3d(vel_cmp, neighbours=ctx.setup.training.velocity.smoothness_neighbours))) for vel_cmp in vel_components]) + tmp_loss_scaled = tmp_loss * loss_scale + ctx.add_loss(tmp_loss_scaled, tmp_loss, loss_scale, 'velocity/edge') + return True + return False +''' + +def loss_vel_smooth(ctx, state, loss_func=None): + '''Smoothness (forward differences) loss for velocity''' + #loss_scale = ctx.CV(ctx.setup.training.velocity.smoothness_loss) + MS_residual_loss = False + loss_scale = ctx.loss_schedules.velocity_smoothness(ctx.iteration) + if ctx.LA(loss_scale): + if loss_func is None: loss_func = ctx.get_loss_func("velocity/smooth") + + norm_MS_total_scales = True and state.velocity.is_MS and state.velocity.recursive_MS_shared_decoder + norm_MS_affected_scales = True and state.velocity.is_MS and state.velocity.recursive_MS_shared_decoder + norm_vel_scale = True + norm_vel_scale_by_world_size = False + + # shape: NDHWC + if ctx.target_weights is None: + norm_axis = [0] + if ctx.norm_spatial_dims: norm_axis += [1,2,3] + elif ctx.norm_spatial_dims: + raise NotImplementedError + + if not (ctx.allow_MS_losses and state.velocity.has_MS_output): + if MS_residual_loss and state.velocity.has_MS_output: + raise NotImplementedError("TODO: MS_residual_loss might not work if backprop does not use MS variables, wich is tied to ctx.allow_MS_losses.") + with ctx.profiler.sample('%s velocity gradient loss'%("residual" if MS_residual_loss else "full")): + tmp_loss = [] + + if norm_vel_scale: + if norm_vel_scale_by_world_size: + raise NotImplementedError + t = state.velocity_tranform + t.grid_size = self.shape_of_scale(scale) + vel_scale = t.cell_size_world() + else: + if state.velocity.is_MS: + scale_factor = state.velocity.recursive_MS_scale_factor + scale = state.velocity._get_top_active_scale() + else: + #raise NotImplementedError + # r64 starting at r4 with scale 2 + scale_factor = 2 + scale = 4 + vel_scale = [1/(scale_factor**scale)]*3 + else: + vel_scale = [1,1,1] + + if state.velocity.is_centered: + vel_components = state.velocity.centered(pad_lod=False, concat=False) + elif state.velocity.is_staggered: + vel_components = state.velocity._staggered() + else: + raise ValueError("Unknown velocity type.") + + for c, s in zip(vel_components, vel_scale): + c = c*s + tmp_loss.append(tf.reduce_mean(loss_func(tf.abs(c[:,:,:,1:,:] - c[:,:,:,:-1,:]), 0), axis=norm_axis)) #x_grad = d[:,:,:,1:,:] - d[:,:,:,:-1,:] + tmp_loss.append(tf.reduce_mean(loss_func(tf.abs(c[:,:,1:,:,:] - c[:,:,:-1,:,:]), 0), axis=norm_axis)) #y_grad = d[:,:,1:,:,:] - d[:,:,:-1,:,:] + tmp_loss.append(tf.reduce_mean(loss_func(tf.abs(c[:,1:,:,:,:] - c[:,:-1,:,:,:]), 0), axis=norm_axis)) #z_grad = d[:,1:,:,:,:] - d[:,:-1,:,:,:] + else: + with ctx.profiler.sample('velocity gradient loss'): + tmp_loss = [] + scales = tuple(state.velocity.gen_current_trainable_MS_scales()) + scale_factor = state.velocity.recursive_MS_scale_factor + for scale in scales: #gen_current_MS_scales(): + + if norm_vel_scale: + if norm_vel_scale_by_world_size: + raise NotImplementedError + t = state.velocity_tranform + t.grid_size = self.shape_of_scale(scale) + vel_scale = t.cell_size_world() + else: + vel_scale = [1/(scale_factor**scale)]*3 + else: + vel_scale = [1,1,1] + + MS_weight = 1.0 + if norm_MS_affected_scales: + MS_weight /= scale + 1 + if norm_MS_total_scales: + MS_weight /= len(scales) + + if state.velocity.is_centered: + if MS_residual_loss: + vel_components = state.velocity.centered_MS_residual(scale, pad_lod=False, concat=False) + else: + vel_components = state.velocity.centered_MS(scale, pad_lod=False, concat=False) + elif state.velocity.is_staggered: + if MS_residual_loss: + vel_components = state.velocity._staggered_MS_residual(scale) + else: + vel_components = state.velocity._staggered_MS(scale) + else: + raise ValueError("Unknown velocity type.") + + for c, s in zip(vel_components, vel_scale): + c = c*s + tmp_loss.append(tf.reduce_mean(loss_func(tf.abs(c[:,:,:,1:,:] - c[:,:,:,:-1,:]), 0), axis=norm_axis)*MS_weight) #x_grad = d[:,:,:,1:,:] - d[:,:,:,:-1,:] + tmp_loss.append(tf.reduce_mean(loss_func(tf.abs(c[:,:,1:,:,:] - c[:,:,:-1,:,:]), 0), axis=norm_axis)*MS_weight) #y_grad = d[:,:,1:,:,:] - d[:,:,:-1,:,:] + tmp_loss.append(tf.reduce_mean(loss_func(tf.abs(c[:,1:,:,:,:] - c[:,:-1,:,:,:]), 0), axis=norm_axis)*MS_weight) #z_grad = d[:,1:,:,:,:] - d[:,:-1,:,:,:] + + # with ctx.profiler.sample('velocity gradient loss'): + # vel_components = state.velocity.var_list() + # tmp_loss = [] + # for c in vel_components: + # tmp_loss.append(tf.reduce_mean(loss_func(c[:,:,:,1:,:] - c[:,:,:,:-1,:], 0), axis=norm_axis)) #x_grad = d[:,:,:,1:,:] - d[:,:,:,:-1,:] + # tmp_loss.append(tf.reduce_mean(loss_func(c[:,:,1:,:,:] - c[:,:,:-1,:,:], 0), axis=norm_axis)) #y_grad = d[:,:,1:,:,:] - d[:,:,:-1,:,:] + # tmp_loss.append(tf.reduce_mean(loss_func(c[:,1:,:,:,:] - c[:,:-1,:,:,:], 0), axis=norm_axis)) #z_grad = d[:,1:,:,:,:] - d[:,:-1,:,:,:] + + tmp_loss_scaled = [_ * loss_scale for _ in tmp_loss] + if ctx.compute_loss_summary(): + ctx.add_loss(tmp_loss_scaled, tf.reduce_sum([tf.reduce_mean(_) for _ in tmp_loss]), tf.reduce_sum([tf.reduce_mean(_) for _ in tmp_loss_scaled]), loss_scale, loss_name='velocity/smooth') + else: + ctx.add_loss(tmp_loss_scaled, None, None, loss_scale, loss_name='velocity/smooth') + return True + return False + +def loss_vel_cossim(ctx, state, loss_func=None): + '''Smoothness (forward differences) loss for velocity''' + #loss_scale = ctx.CV(ctx.setup.training.velocity.smoothness_loss) + loss_scale = ctx.loss_schedules.velocity_cossim(ctx.iteration) + if ctx.LA(loss_scale): + raise NotImplementedError("Implement normalization") + if loss_func is None: loss_func = ctx.get_loss_func("velocity/cossim") + with ctx.profiler.sample('velocity cosine loss'): + v = state.velocity.centered() + tmp_loss = [ + loss_func(tf_cosine_similarity(v[:,:,:,1:,:], v[:,:,:,:-1,:], axis=-1)*(-0.5)+0.5, 0), + loss_func(tf_cosine_similarity(v[:,:,1:,:,:], v[:,:,:-1,:,:], axis=-1)*(-0.5)+0.5, 0), + loss_func(tf_cosine_similarity(v[:,1:,:,:,:], v[:,:-1,:,:,:], axis=-1)*(-0.5)+0.5, 0), + ] + + tmp_loss_scaled = [_ * loss_scale for _ in tmp_loss] + if ctx.compute_loss_summary(): + ctx.add_loss(tmp_loss_scaled, tf.reduce_sum([tf.reduce_mean(_) for _ in tmp_loss]), tf.reduce_sum([tf.reduce_mean(_) for _ in tmp_loss_scaled]), loss_scale, loss_name='velocity/cossim') + else: + ctx.add_loss(tmp_loss_scaled, None, None, loss_scale, loss_name='velocity/cossim') + return True + return False + +def loss_vel_divergence(ctx, state, loss_func=None): + '''divergence loss''' + #loss_scale = ctx.CV(ctx.setup.training.velocity.divergence_loss) + loss_scale = ctx.loss_schedules.velocity_divergence(ctx.iteration) + if ctx.LA(loss_scale): + if ctx.target_weights is None: + norm_axis = [0] + if ctx.norm_spatial_dims: norm_axis += [1,2,3] + elif ctx.norm_spatial_dims: + raise NotImplementedError + + if loss_func is None: loss_func = ctx.get_loss_func("velocity/divergence") + LOG.debug("Run divergence loss for velocity") + if not (ctx.allow_MS_losses and state.velocity.has_MS_output): + with ctx.profiler.sample('divergence loss'): + tmp_loss = [tf.reduce_mean(loss_func(state.velocity.divergence(ctx.vel_scale), 0), axis=norm_axis)] + else: + with ctx.profiler.sample('divergence loss MS'): + tmp_loss = [] + for scale in state.velocity.gen_current_MS_scales(): + MS_weight = ctx.loss_schedules.velocity_divergence_MS_weighting(scale) + tmp_loss.append( tf.reduce_mean(MS_weight * loss_func(state.velocity.divergence_MS(scale, ctx.vel_scale), 0), axis=norm_axis) ) + tmp_loss_scaled = [_ * loss_scale for _ in tmp_loss] + # if setup.training.velocity.divergence_normalize>0: + # raise NotImplementedError() + if ctx.compute_loss_summary(): + ctx.add_loss(tmp_loss_scaled, tf.reduce_sum([tf.reduce_mean(_) for _ in tmp_loss]), tf.reduce_sum([tf.reduce_mean(_) for _ in tmp_loss_scaled]), loss_scale, 'velocity/divergence') + else: + ctx.add_loss(tmp_loss_scaled, None, None, loss_scale, 'velocity/divergence') + return True + return False + +def loss_vel_magnitude(ctx, state, loss_func=None): + ''' + loss to minimize velocities + tf.norm can cause issues (NaN gradients at 0 magnitude): https://github.com/tensorflow/tensorflow/issues/12071 + ''' + loss_scale = ctx.loss_schedules.velocity_magnitude(ctx.iteration) + if ctx.LA(loss_scale): + if loss_func is None: loss_func = ctx.get_loss_func("velocity/magnitude") + + norm_MS_total_scales = True and state.velocity.is_MS and state.velocity.recursive_MS_shared_decoder + norm_MS_affected_scales = True and state.velocity.is_MS and state.velocity.recursive_MS_shared_decoder + norm_vel_scale = True + norm_vel_scale_by_world_size = False + + + # shape: NDHWC + if ctx.target_weights is None: + norm_axis = [0] + if ctx.norm_spatial_dims: norm_axis += [1,2,3] + elif ctx.norm_spatial_dims: + raise NotImplementedError + + LOG.debug("Run vector magnitude loss for velocity") + if not (ctx.allow_MS_losses and state.velocity.has_MS_output): + with ctx.profiler.sample('magnitude loss'): + if norm_vel_scale: + if norm_vel_scale_by_world_size: + raise NotImplementedError + t = state.velocity_tranform + t.grid_size = self.shape_of_scale(scale) + vel_scale = t.cell_size_world() + else: + if state.velocity.is_MS: + scale_factor = state.velocity.recursive_MS_scale_factor + scale = state.velocity._get_top_active_scale() + else: + #raise NotImplementedError + # r64 starting at r4 with scale 2 + scale_factor = 2 + scale = 4 + vel_scale = [1/(scale_factor**scale)]*3 + else: + vel_scale = [1,1,1] + tmp_loss = [tf.reduce_mean(loss_func(state.velocity.magnitude(vel_scale), 0), axis=norm_axis)] + else: + with ctx.profiler.sample('magnitude loss MS'): + tmp_loss = [] + scales = tuple(state.velocity.gen_current_trainable_MS_scales()) + scale_factor = state.velocity.recursive_MS_scale_factor + for scale in scales: + + if norm_vel_scale: + if norm_vel_scale_by_world_size: + raise NotImplementedError + t = state.velocity_tranform + t.grid_size = self.shape_of_scale(scale) + vel_scale = t.cell_size_world() + else: + vel_scale = [1/(scale_factor**scale)]*3 + else: + vel_scale = [1,1,1] + + MS_weight = ctx.loss_schedules.velocity_magnitude_MS_weighting(scale) + if norm_MS_affected_scales: + MS_weight /= scale + 1 + if norm_MS_total_scales: + MS_weight /= len(scales) + + tmp_loss.append(tf.reduce_mean(loss_func(state.velocity.magnitude_MS(scale, vel_scale), 0), axis=norm_axis) * MS_weight ) + tmp_loss_scaled = [_ * loss_scale for _ in tmp_loss] + if ctx.compute_loss_summary(): + ctx.add_loss(tmp_loss_scaled, tf.reduce_sum([tf.reduce_mean(_) for _ in tmp_loss]), tf.reduce_sum([tf.reduce_mean(_) for _ in tmp_loss_scaled]), loss_scale, 'velocity/magnitude') + else: + ctx.add_loss(tmp_loss_scaled, None, None, loss_scale, 'velocity/magnitude') + return True + return False + +def loss_vel_CFLcond(ctx, state, loss_func=None): + '''loss to minimize velocities''' + loss_scale = ctx.loss_schedules.velocity_CFLcond(ctx.iteration) + if ctx.LA(loss_scale): + if loss_func is None: loss_func = ctx.get_loss_func("velocity/CFL") + loss_before_cutoff = True + MS_residual_loss = True + distance_L2 = False + Cmax = 1.0 + LOG.debug("Run vector magnitude loss (CFL) for velocity") + # with ctx.profiler.sample('CFL condition loss'): + # vel_x, vel_y, vel_z = state.velocity.centered(pad_lod=False, concat=False) + # #https://en.wikipedia.org/wiki/Courant%E2%80%93Friedrichs%E2%80%93Lewy_condition ; cell-size=1, dt=1, Cmax=1 + # tmp_loss = loss_func(tf.maximum((vel_x + vel_y + vel_z) - 1.0, 0.0), 0.0) + # tmp_loss_scaled = tmp_loss * loss_scale + # ctx.add_loss(tmp_loss_scaled, tmp_loss, loss_scale, 'velocity/CFL') + + if loss_before_cutoff: + loss_fn = lambda mag: tf.maximum(loss_func(mag, 0.0) - Cmax, 0.0) + else: + loss_fn = lambda mag: loss_func(tf.maximum(mag - Cmax, 0.0), 0.0) + + # shape: NDHWC + if ctx.target_weights is None: + norm_axis = [0] + if ctx.norm_spatial_dims: norm_axis += [1,2,3] + elif ctx.norm_spatial_dims: + raise NotImplementedError + + if not (ctx.allow_MS_losses and state.velocity.has_MS_output): + if MS_residual_loss and state.velocity.has_MS_output: + raise NotImplementedError("TODO: MS_residual_loss might not work if backprop does not use MS variables, wich is tied to ctx.allow_MS_losses.") + with ctx.profiler.sample('CFL loss%s%s'%(" residual" if MS_residual_loss else " full", " centered" if state.velocity.is_centered else " staggered")): + # if distance_L2: + # mag = state.velocity.magnitude() + # else: + # vel_x, vel_y, vel_z = state.velocity.centered(pad_lod=False, concat=False) + # mag = tf.abs(vel_x) + tf.abs(vel_y) + tf.abs(vel_z) + # tmp_loss = [loss_fn(mag)] + tmp_loss = [] + if distance_L2: + if MS_residual_loss and state.velocity.has_MS_output: + mag = state.velocity.magnitude_MS_residual(state.velocity._get_top_active_scale()) + else: + mag = state.velocity.magnitude() + tmp_loss.append(tf.reduce_mean(loss_fn(mag), axis=norm_axis)) + else: + if state.velocity.is_centered: + if MS_residual_loss and state.velocity.has_MS_output: + vel_components = state.velocity.centered_MS_residual(state.velocity._get_top_active_scale(), pad_lod=False, concat=False) + else: + vel_components = state.velocity.centered(pad_lod=False, concat=False) + elif state.velocity.is_staggered: + if MS_residual_loss and state.velocity.has_MS_output: + vel_components = state.velocity._staggered_MS_residual(state.velocity._get_top_active_scale()) + else: + vel_components = state.velocity._staggered() + else: + raise ValueError("Unknown velocity type.") + for vel_comp in vel_components: + tmp_loss.append(tf.reduce_mean(loss_fn(tf.abs(vel_comp)), axis=norm_axis)) + else: + with ctx.profiler.sample('CFL loss MS%s%s'%(" residual" if MS_residual_loss else " full", " centered" if state.velocity.is_centered else " staggered")): + tmp_loss = [] + for scale in state.velocity.gen_current_trainable_MS_scales(): #gen_current_MS_scales(): + MS_weight = ctx.loss_schedules.velocity_CFLcond_MS_weighting(scale) + if distance_L2: + if MS_residual_loss: + mag = state.velocity.magnitude_MS_residual(scale) + else: + mag = state.velocity.magnitude_MS(scale) + tmp_loss.append(MS_weight * tf.reduce_mean(loss_fn(mag), axis=norm_axis)) + else: + if state.velocity.is_centered: + if MS_residual_loss: + vel_components = state.velocity.centered_MS_residual(scale, pad_lod=False, concat=False) + else: + vel_components = state.velocity.centered_MS(scale, pad_lod=False, concat=False) + elif state.velocity.is_staggered: + if MS_residual_loss: + vel_components = state.velocity._staggered_MS_residual(scale) + else: + vel_components = state.velocity._staggered_MS(scale) + else: + raise ValueError("Unknown velocity type.") + for vel_comp in vel_components: + tmp_loss.append(MS_weight * tf.reduce_mean(loss_fn(tf.abs(vel_comp)), axis=norm_axis)) + tmp_loss_scaled = [_ * loss_scale for _ in tmp_loss] + if ctx.compute_loss_summary(): + ctx.add_loss(tmp_loss_scaled, tf.reduce_sum([tf.reduce_mean(_) for _ in tmp_loss]), tf.reduce_sum([tf.reduce_mean(_) for _ in tmp_loss_scaled]), loss_scale, 'velocity/CFL') + else: + ctx.add_loss(tmp_loss_scaled, None, None, loss_scale, 'velocity/CFL') + return True + return False + +def loss_vel_MS_coherence(ctx, state, loss_func=None): + #use highest vel output to constrain lower vel scales + #loss_scale = ctx.CV(ctx.setup.training.velocity.density_warp_loss) + loss_scale = ctx.loss_schedules.velocity_MS_coherence(ctx.iteration) + top_as_label = True + + if ctx.LA(loss_scale): + if loss_func is None: loss_func = ctx.get_loss_func("velocity/MS_coherence") + # shape: NDHWC + if ctx.target_weights is None: + norm_axis = [0] + if ctx.norm_spatial_dims: norm_axis += [1,2,3] + elif ctx.norm_spatial_dims: + raise NotImplementedError + + if not (ctx.allow_MS_losses and state.velocity.has_MS_output): + LOG.warning("Loss 'velocity/MS_coherence' is active, but velocity has no MS or MS losses are not allowed.") + return False + else: + LOG.debug("Run multi-scale coherence loss for velocity.") + with ctx.profiler.sample('velocity MS coherence loss'): + tmp_loss = [] + vel_MS_scales = list(reversed(list(state.velocity.gen_current_MS_scales())[:-1])) #fine to coarse, starting at 2nd highest + if len(vel_MS_scales)<1: + LOG.debug("Insuficient scales for velocity multi-scale coherence loss.") + return False + + if state.velocity.is_centered: + last_scale_vel = state.velocity.centered_MS(state.velocity._get_top_active_scale()) + + for scale in vel_MS_scales: #fine to coarse, starting at 2nd highest + MS_weight = ctx.loss_schedules.velocity_MS_coherence_MS_weighting(scale) + # what is better, sampling always from the finest scale or only from the next finer? (regarding performance and gradient quality) + last_scale_vel = state.velocity.resample_velocity(state.velocity.scale_renderer, last_scale_vel, shape=state.velocity.centered_shape_of_scale(scale), \ + is_staggered=state.velocity.is_staggered, scale_magnitude=True) + if top_as_label: + last_scale_vel = tf.stop_gradient(last_scale_vel) + + tmp_loss.append( MS_weight * tf.reduce_mean(loss_func(last_scale_vel, state.velocity.centered_MS(scale)), axis=norm_axis)) + + elif state.velocity.is_staggered: + last_scale_vel = state.velocity._staggered_MS(state.velocity._get_top_active_scale()) # (x,y,z) + last_scale_vel = tuple(last_scale_vel) + + for scale in vel_MS_scales: #fine to coarse, starting at 2nd highest + MS_weight = ctx.loss_schedules.velocity_MS_coherence_MS_weighting(scale) + # what is better, sampling always from the finest scale or only from the next finer? (regarding performance and gradient quality) + last_scale_vel = state.velocity.resample_velocity(state.velocity.scale_renderer, last_scale_vel, shape=state.velocity.centered_shape_of_scale(scale), \ + is_staggered=state.velocity.is_staggered, scale_magnitude=True) + if top_as_label: + last_scale_vel = tuple(tf.stop_gradient(_) for _ in last_scale_vel) + + tmp_loss.append( MS_weight * tf.reduce_mean(loss_func(last_scale_vel[0], state.velocity._staggered_MS(scale)[0]), axis=norm_axis)) #x + tmp_loss.append( MS_weight * tf.reduce_mean(loss_func(last_scale_vel[1], state.velocity._staggered_MS(scale)[1]), axis=norm_axis)) #y + tmp_loss.append( MS_weight * tf.reduce_mean(loss_func(last_scale_vel[2], state.velocity._staggered_MS(scale)[2]), axis=norm_axis)) #z + else: + raise ValueError("Unknown velocity type.") + + + tmp_loss_scaled = [_*loss_scale for _ in tmp_loss] + if ctx.compute_loss_summary(): + ctx.add_loss(tmp_loss_scaled, tf.reduce_sum([tf.reduce_mean(_) for _ in tmp_loss]), tf.reduce_sum([tf.reduce_mean(_) for _ in tmp_loss_scaled]), loss_scale, 'velocity/MS_coherence') + else: + ctx.add_loss(tmp_loss_scaled, None, None, loss_scale, 'velocity/MS_coherence') + return True + return False + +def warp_vel_grads(opt_ctx, state, grads, order='FWD'): + raise NotImplementedError + if order.upper()=='FWD': #propagate velocity gradients to next state, simple forward warp + v = state.veloctiy + grads = VelocityGrid(v.centered_shape, x=grads[0], y=grads[1], z=grads[2], as_var=False, boundary=v.boundary, \ + warp_renderer=v.warp_renderer, scale_renderer=v.scale_renderer) + return grads.warped(vel_grid=v, order=opt_ctx.warp_order, dt=opt_ctx.dt, clamp=opt_ctx.vel_warp_clamp) + elif order.upper()=='BWD': #propagate velocity gradients to previous state, backprop through prev->warp + var_list = state.velocity.var_list() + with tf.GradientTape(watch_accessed_variables=False) as tape: + tape.watch(var_list) + v_warp = state.velocity.warped(order=opt_ctx.warp_order, dt=opt_ctx.dt, clamp=opt_ctx.vel_warp_clamp) + return tape.gradient(v_warp, var_list, grads) + else: + raise ValueError + +def optStep_velocity(opt_ctx, state, custom_vel_grads=None, optimize_inflow=False, apply_vel_grads=False): + with opt_ctx.profiler.sample('optStep_velocity'): + with opt_ctx.profiler.sample('get variables'): + #vel_var_list = state.velocity.var_list() + #vel_vars = state.velocity.get_variables() + vel_vars = state.velocity.get_output_variables(include_MS=opt_ctx.allow_MS_losses, include_residual=True, only_trainable=True) #this generates velocity if using a network + if optimize_inflow: + dens_vars = state.density.get_variables() + if 'inflow' in dens_vars: # inflow variable available + vel_vars['inflow'] = dens_vars['inflow'] + if opt_ctx.setup.training.optimize_buoyancy: + #vel_var_list.append(opt_ctx.buoyancy) + vel_vars['buoyancy'] = opt_ctx.buoyancy + #LOG.info("Velocity output variables: %s", list(vel_vars.keys())) + with opt_ctx.profiler.sample('loss'), tf.GradientTape(watch_accessed_variables=False, persistent=opt_ctx.inspect_gradients) as vel_tape: + vel_tape.watch(vel_vars) + if opt_ctx.inspect_gradients: + vel_inspect_vars = {} + vel_inspect_vars.update(vel_vars) + # if "velocity_decoder" in vel_vars: + # vel_inspect_vars["velocity_c"] = state.velocity.centered() + # vel_inspect_vars["velocity_x"] = state.velocity._x + # vel_inspect_vars["velocity_y"] = state.velocity._y + # vel_inspect_vars["velocity_z"] = state.velocity._z + # else: + # vel_inspect_vars["velocity_x"] = vel_vars["velocity_x"] + # vel_inspect_vars["velocity_y"] = vel_vars["velocity_y"] + # vel_inspect_vars["velocity_z"] = vel_vars["velocity_z"] + if "inflow" in vel_vars: vel_inspect_vars["inflow"] = vel_vars["inflow"] + if "buoyancy" in vel_vars: vel_inspect_vars["buoyancy"] = vel_vars["buoyancy"] + #vel_tape.watch(vel_inspect_vars) + opt_ctx.set_gradient_tape(vel_tape) + # velocity_loss = 0 + active_velocity_loss = False + LOG.debug("velocity losses") + + #warp losses + active_velocity_loss = loss_vel_target_vol(opt_ctx, state) or active_velocity_loss + active_velocity_loss = loss_vel_warp_dens(opt_ctx, state) or active_velocity_loss + active_velocity_loss = loss_vel_warp_dens_proxy(opt_ctx, state) or active_velocity_loss + active_velocity_loss = loss_vel_warp_dens_target(opt_ctx, state) or active_velocity_loss + active_velocity_loss = loss_vel_warp_vel(opt_ctx, state) or active_velocity_loss + + #direct losses + active_velocity_loss = loss_vel_smooth(opt_ctx, state) or active_velocity_loss + active_velocity_loss = loss_vel_cossim(opt_ctx, state) or active_velocity_loss + active_velocity_loss = loss_vel_divergence(opt_ctx, state) or active_velocity_loss + active_velocity_loss = loss_vel_magnitude(opt_ctx, state) or active_velocity_loss + active_velocity_loss = loss_vel_CFLcond(opt_ctx, state) or active_velocity_loss + active_velocity_loss = loss_vel_MS_coherence(opt_ctx, state) or active_velocity_loss + + # velocity_loss = opt_ctx.loss #opt_ctx.pop_loss() + #END gradient tape + if active_velocity_loss: + with opt_ctx.profiler.sample('gradient'): + if custom_vel_grads is not None: + cvg_scale = opt_ctx.CV(opt_ctx.custom_vel_grads_weight) + if opt_ctx.inspect_gradients: + for loss_name in opt_ctx._losses: + vel_grads = vel_tape.gradient(opt_ctx.get_loss(loss_name), vel_inspect_vars) + #LOG.debug("vel grads: %s", [_ for _ in vel_grads]) + for k, g in vel_grads.items(): + if k.startswith('velocity_') and k[-2:] in ["_c", "_x", "_y", "_z"]: + if g is not None: + opt_ctx.inspect_gradients_func(opt_ctx=opt_ctx, gradients=g, name=loss_name+k[8:]) + del vel_grads + + if custom_vel_grads is not None and opt_ctx.LA(cvg_scale): + has_valid_vel_grads = False + for k, g in custom_vel_grads.items(): + if k.startswith('velocity_') and k[-2:] in ["_c", "_x", "_y", "_z"]: + if g is not None: + has_valid_vel_grads = True + opt_ctx.inspect_gradients_func(opt_ctx=opt_ctx, gradients=g, name="velocity/custom_grad"+k[8:]) + else: + LOG.debug("Custom velocity gradient of frame %d for '%s' is None.", state.frame, k) + if not has_valid_vel_grads: + LOG.warning("All custom velocity gradients of frame %d are None.", state.frame) + opt_ctx.inspect_gradients_images = {} + + vel_grads = vel_tape.gradient(opt_ctx.get_losses(), vel_inspect_vars) + + #if opt_ctx.inspect_gradients: + has_valid_vel_grads = False + for k, g in vel_grads.items(): + if k.startswith('velocity_') and k[-2:] in ["_c", "_x", "_y", "_z"]: + if g is not None: + has_valid_vel_grads = True + opt_ctx.inspect_gradients_func(opt_ctx=opt_ctx, gradients=g, name="velocity/_local"+k[8:]) + else: + LOG.debug("Total gradient of frame %d for '%s' is None.", state.frame, k) + if not has_valid_vel_grads: + LOG.warning("All local velocity gradients of frame %d are None.", state.frame) + + if vel_grads.get('buoyancy') is not None: + opt_ctx.inspect_gradients_func(opt_ctx=opt_ctx, gradients=vel_grads['buoyancy'][0], name="velocity/buoyancy_x") + opt_ctx.inspect_gradients_func(opt_ctx=opt_ctx, gradients=vel_grads['buoyancy'][1], name="velocity/buoyancy_y") + opt_ctx.inspect_gradients_func(opt_ctx=opt_ctx, gradients=vel_grads['buoyancy'][2], name="velocity/buoyancy_z") + if vel_grads.get('inflow') is not None: + opt_ctx.inspect_gradients_func(opt_ctx=opt_ctx, gradients=vel_grads['inflow'], name="velocity/inflow") + + if custom_vel_grads is not None and opt_ctx.allow_MS_losses: + # back-warping give gradients w.r.t the the output variables, not the MS-output variables, so map to the highest MS-output. + custom_vel_grads_inspect = state.velocity.map_gradient_output_to_MS(custom_vel_grads) + else: + custom_vel_grads_inspect = custom_vel_grads + + has_valid_vel_grads = False + for k in vel_grads: #depth 1 sufficient for now... + if custom_vel_grads is not None and opt_ctx.LA(cvg_scale): + if k in custom_vel_grads_inspect: + if vel_grads[k] is not None and custom_vel_grads_inspect[k] is not None: + LOG.info("Update velocity gradient '%s' with custom gradient with scale %f.", k, cvg_scale) + vel_grads[k] += custom_vel_grads_inspect[k]*cvg_scale + elif vel_grads[k] is None and custom_vel_grads_inspect[k] is not None: + LOG.info("Set velocity gradient '%s' to custom gradient with scale %f.", k, cvg_scale) + vel_grads[k] = custom_vel_grads_inspect[k]*cvg_scale + else: + LOG.debug("Custom velocity gradient '%s' of frame %d is None", k, state.frame) + if vel_grads[k] is not None: + has_valid_vel_grads = True + # opt_ctx.inspect_gradients_func(opt_ctx=opt_ctx, gradients=vel_grads[k], name="velocity/_total"+k[8:]) + if not has_valid_vel_grads: + LOG.error("All velocity gradients of frame %d are None.", state.frame) + + LOG.debug('Compute and apply velocity gradients') + vel_grads = vel_tape.gradient(opt_ctx.get_losses(), vel_vars) + + curr_vel_grads = copy_nested_structure(vel_grads) + + if custom_vel_grads is not None and opt_ctx.LA(cvg_scale): + + if opt_ctx.allow_MS_losses: + # back-warping give gradients w.r.t the the output variables, not the MS-output variables, so map to the highest MS-output. + custom_vel_grads = state.velocity.map_gradient_output_to_MS(custom_vel_grads) + + #vel_grads = [v + c*cvg_scale for v,c in zip(vel_grads[:len(custom_vel_grads)], custom_vel_grads)] + vel_grads[len(custom_vel_grads):] + for k in vel_grads: #depth 1 sufficient for now... + if k in custom_vel_grads: + if custom_vel_grads[k] is not None: + LOG.debug("Update velocity gradient '%s' with custom gradient.", k) + if vel_grads[k] is None: + #LOG.warning("This should crash now...") + vel_grads[k] = custom_vel_grads[k]*cvg_scale + else: + vel_grads[k] += custom_vel_grads[k]*cvg_scale + #backprop_accumulate hadles this already + # elif vel_grads[k] is None: + # vel_grads[k] = 0.0 + #else: + # LOG.debug("Custom velocity gradient '%s' of frame %d is None", k, state.frame) + + for k in custom_vel_grads: + if k not in vel_grads: + LOG.warning("Custom velocity gradient '%s' can't be mapped and will be ignored.", k) + + if apply_vel_grads: + if vel_grads.get('inflow') is not None: + inflow_grads_vars = ((vel_grads['inflow'], vel_vars['inflow']),) + opt_ctx.density_optimizer.apply_gradients(inflow_grads_vars) + del vel_grads['inflow'] + del vel_vars['inflow'] + # vel_grads_vars = zip(nest.flatten(vel_grads), nest.flatten(vel_vars)) + # opt_ctx.velocity_optimizer.apply_gradients(vel_grads_vars) + state.velocity.set_output_gradients_for_backprop_accumulate(vel_grads, include_MS=opt_ctx.allow_MS_losses, include_residual=True, only_trainable=True) + + if apply_vel_grads: + #state.velocity.backprop_accumulate(vel_grads, include_MS=opt_ctx.allow_MS_losses, include_residual=True, only_trainable=True) + state.velocity._compute_input_grads() + else: + #curr_vel_grads = nest.map_structure(lambda v: tf.constant(0, dtype=tf.float32), vel_vars) #[tf.constant(0, dtype=tf.float32) for _ in vel_var_list] + curr_vel_grads = nest.map_structure(lambda v: tf.zeros_like(v), vel_vars) + state.velocity.set_output_gradients_for_backprop_accumulate(curr_vel_grads, include_MS=opt_ctx.allow_MS_losses, include_residual=True, only_trainable=True) + del vel_tape + return active_velocity_loss, curr_vel_grads + +def optStep_state(opt_ctx, state, disc_ctx=None, disc_samples_list=None, custom_dens_grads=None, custom_vel_grads=None, apply_dens_grads=False, apply_vel_grads=False): + LOG.debug("Optimization step for state: frame %d", state.frame) + with opt_ctx.profiler.sample('optStep_state'): + prev_losses = opt_ctx._losses + opt_ctx.pop_losses() + opt_ctx.frame = state.frame + + # density_proxy optimization here, the velocity might need the gradients + # TODO? + + # velocity optimization, uses the density, so the density needs the gradients + if (state.next is not None): #the last frame has no target for a velocity + vel_active, vel_grads = optStep_velocity(opt_ctx, state, custom_vel_grads=custom_vel_grads, apply_vel_grads=apply_vel_grads) + #velocity_loss = opt_ctx.pop_losses() + opt_ctx.pop_losses() + else: + vel_active = False + vel_grads = None + + dens_active, dens_grads = optStep_density(opt_ctx, state, use_vel=True, disc_ctx=disc_ctx, disc_samples_list=disc_samples_list, custom_dens_grads=custom_dens_grads, apply_dens_grads=apply_dens_grads) + + opt_ctx.pop_losses() + opt_ctx._losses = prev_losses + + #return dens_grads, vel_grads + +def optStep_sequence(opt_ctx, sequence, disc_ctx=None, disc_samples_list=None, order='FWD'): + LOG.debug("Optimization step for sequence with %d states", len(sequence)) + total_losses = [] + loss_summaries = {} + if order.upper()=='FWD': + optim_order = list(range(len(sequence))) + elif order.upper()=='BWD': + optim_order = list(reversed(range(len(sequence)))) + elif order.upper()=='RAND': + optim_order = np.random.permutation(len(sequence)).tolist() + else: + raise ValueError + + #warnings.warn("Optimizing only first frame of sequence.") + for i in optim_order: #[0]: # + state = sequence[i] + + with opt_ctx.profiler.sample("Frame"): + optStep_state(opt_ctx, state, disc_ctx, disc_samples_list) + loss_summaries[state.frame]=opt_ctx.pop_loss_summary() + + return loss_summaries + +### Discriminator +class DiscriminatorContext: + CHECK_INPUT_NONE = 0x0 + CHECK_INPUT_CLAMP = 0x1 + CHECK_INPUT_DUMP = 0x2 + CHECK_INPUT_CHECK = 0x10 + CHECK_INPUT_CHECK_NOTFINITE = 0x20 + CHECK_INPUT_SIZE = 0x40 + CHECK_INPUT_RAISE = 0x100 + CHECK_INPUT_RAISE_NOTFINITE = 0x200 + def __init__(self, ctx, model, rendering_context, real_data, loss_type, optimizer, learning_rate, crop_size=None, scale_range=[1,1], rotation_mode="NONE", check_input=0, check_info_path=None, \ + resource_device=None, scale_samples_to_input_resolution=False, \ + use_temporal_input=False, temporal_input_steps=None, cam_x_range=[-30,30]): + assert isinstance(ctx, OptimizationContext) + assert isinstance(rendering_context, RenderingContext) + assert isinstance(learning_rate, tf.Variable) + #raise NotImplementedError() + self.model = model + self.real_data = real_data + self.opt_ctx = ctx + self.render_ctx = rendering_context + self.cam_x_range = cam_x_range + self.history = None + self._train_base = True + self._train = True + if ctx.setup.training.discriminator.history.samples>0: + self.history = HistoryBuffer(ctx.setup.training.discriminator.history.size) + #self._label_fake = 0.0 + self._label_real = ctx.setup.training.discriminator.target_label + self._conditional_hull = ctx.setup.training.discriminator.conditional_hull + self.optimizer = optimizer + self.lr = learning_rate + self._last_it = self.opt_ctx.iteration + self.center_crop = True # center crop on density center of mass + random offset + self.crop_size = crop_size + self.scale_range = scale_range + self.rotation_mode = rotation_mode + self.dump_path = None + self._check_input = check_input #bit-mask + self._check_info_path = check_info_path + self._input_range = [0.0, 10.0] + self.resource_device = resource_device + loss_types = ["SGAN", "RpSGAN", "RpLSGAN", "RaSGAN", "RaLSGAN"] + if loss_type not in loss_types: + raise ValueError("Unknown Discriminator loss_type {}. Available losses: {}".format(loss_type, loss_types)) + self.loss_type = loss_type #SGAN, RpSGAN, RaSGAN, RcSGAN + + self.scale_to_input_res = scale_samples_to_input_resolution + #self.input_res = model.input_shape[-3:-1] #input_base_resolution + + self._temporal_input = use_temporal_input + self._temporal_input_steps = temporal_input_steps + + @property + def train(self): + return self._train and self._train_base + + @train.setter + def train(self, train): + self._train_base = train + + def start_iteration(self, iteration, force=False, compute_loss_summary=False): + self.opt_ctx.start_iteration(iteration, force, compute_loss_summary) + if iteration==self._last_it and not force: + return + + curr_lr = self.opt_ctx.loss_schedules.discriminator_lr(self.opt_ctx.iteration - self.opt_ctx.setup.training.discriminator.start_delay) + self._train = self.opt_ctx.LA(curr_lr) + self.lr.assign(curr_lr) + + if self.opt_ctx.record_summary: + summary_names = self.opt_ctx.make_summary_names('discriminator/learning_rate') + self.opt_ctx._tf_summary.scalar(summary_names[0], self.lr.numpy(), step=self.opt_ctx.iteration) + self._last_it = self.opt_ctx.iteration + + @property + def input_res(self): + return self.model.input_shape[-3:-1] + + @property + def record_history(self): + return self.train and self.history is not None + + def var_list(self): + return self.model.trainable_variables + + def real_labels(self, logits): + return tf.ones_like(logits)*self._label_real + def fake_labels(self, logits): + return tf.zeros_like(logits) + + def _scale_range(self, shape, target_shape, max_scale_range): + scale = np.amax([t/i for t,i in zip(target_shape, shape)]) + if scale>max_scale_range[1]: #scale0: + nan_in = tf.reduce_any(tf.is_nan(input), axis=[1,2,3]) + if tf.reduce_any(nan_in).numpy(): + LOG.warning("NaN in samples %s of discriminator input '%s' in iteration %d.", np.where(nan_in.numpy())[0], name, self.opt_ctx.iteration) + dump_input = True + nan_input = True + if (self._check_input & self.CHECK_INPUT_SIZE)>0: + input_shape = shape_list(input) + if tf.reduce_any(tf.not_equal(input.get_shape()[-3:], self.model.input_shape[-3:])).numpy(): + LOG.warning("shape %s of input '%s' does not match discriminator input shape %s", shape_list(input), name, self.model.input_shape) + if (self._check_input & self.CHECK_INPUT_CHECK)>0: + if tf.reduce_any(tf.less(input, self._input_range[0])).numpy(): + in_min = tf.reduce_min(input).numpy() + LOG.warning("Minimum value %f of discriminator input '%s' exceeds minimum %f in iteration %d.", in_min, name, self._input_range[0], self.opt_ctx.iteration) + dump_input = True + if tf.reduce_any(tf.greater(input, self._input_range[1])).numpy(): + in_max = tf.reduce_max(input).numpy() + LOG.warning("Maximum value %f of discriminator input '%s' exceeds maximum %f in iteration %d.", in_max, name, self._input_range[1], self.opt_ctx.iteration) + dump_input = True + if dump_input and (self._check_input & self.CHECK_INPUT_DUMP)>0 and self._check_info_path is not None: + name = "{}_{:06d}".format(name, self.opt_ctx.iteration) + "_{:04d}" + self.render_ctx.dens_renderer.write_images([input], [name], base_path=self._check_info_path, use_batch_id=True, format='EXR') + if (dump_input and (self._check_input & self.CHECK_INPUT_RAISE)>0) or (nan_input and (self._check_input & self.CHECK_INPUT_RAISE_NOTFINITE)>0): + raise ValueError("Discriminator input {} error.".format(name)) + + + if (self._check_input & self.CHECK_INPUT_CLAMP)>0: + return tf.minimum(tf.maximum(input, self._input_range[0]), self._input_range[1]) #also makes nan and -inf to min and inf to max (TF 1.12 on GPU) + else: + return input + + def check_output(self, output, loss, input, name='output'): + if (self._check_input & (self.CHECK_INPUT_CHECK_NOTFINITE | self.CHECK_INPUT_CHECK))>0: + dump_input = False + if not tf.reduce_all(tf.is_finite(output)): + LOG.warning("Discriminator output '%s' in iteration %d is not finite: %s", name, self.opt_ctx.iteration, output.numpy()) + dump_input = True + if not tf.reduce_all(tf.is_finite(loss)): + LOG.warning("Discriminator loss '%s' in iteration %d is not finite: %s", name, self.opt_ctx.iteration, loss.numpy()) + dump_input = True + if dump_input and (self._check_input & self.CHECK_INPUT_DUMP)>0 and self._check_info_path is not None: + file_name = "{}_{:06d}".format(name, self.opt_ctx.iteration) + "_{:04d}" + self.render_ctx.dens_renderer.write_images([input], [file_name], base_path=self._check_info_path, use_batch_id=True, format='EXR') + if dump_input and (self._check_input & (self.CHECK_INPUT_RAISE_NOTFINITE |self.CHECK_INPUT_RAISE))>0: + raise ValueError("Discriminator output {} error.".format(name)) + + def _scale_samples_to_input_res(self, *samples_raw): + if self.scale_to_input_res: + #LOG.info("Scaling disc input before augmentation from %s to %s.", [shape_list(_) for _ in samples_raw], self.input_res) + return [tf.image.resize_bilinear(sample_raw, self.input_res) for sample_raw in samples_raw] + else: + return samples_raw + + def _pad_samples_to_input_res(self, *samples_raw): + return [tf_pad_to_shape(sample_raw, [-1]+ list(self.input_res) +[-1], allow_larger_dims=True) for sample_raw in samples_raw] + + def image_center_of_mass(self, img): + sample_mean_y = tf.reduce_mean(sample, axis=[-3,-1]) #NW + # LOG.info("mean y shape: %s", sample_mean_y.get_shape().as_list()) + coords_x = tf.reshape(tf.range(0, scale_shape[-1], 1,dtype=tf.float32), (1,scale_shape[-1])) #1W + center_x = tf.reduce_sum(coords_x*sample_mean_y, axis=-1)/tf.reduce_sum(sample_mean_y, axis=-1) #N + sample_mean_x = tf.reduce_mean(sample, axis=[-2,-1]) #NH + coords_y = tf.reshape(tf.range(0, scale_shape[-2], 1,dtype=tf.float32), (1,scale_shape[-2])) #1H + center_y = tf.reduce_sum(coords_y*sample_mean_x, axis=-1)/tf.reduce_sum(sample_mean_x, axis=-1) #N + return Float2(center_x, center_y) + + #def augment_spatial(self, *samples, scale_range=(1.,1.), rotation_mode="90", out_shape="SAME"): + # # use renderer to do scaling, rotation, crop and CoM shift in one step + # # renderer only support 1,2,4 channels, we have 3 or 9. mipmapping for 2D is also not working. + # out_samples = [] + # for sample in samples: + # sample_shape = GridShape( + # with self.opt_ctx.profiler.sample('augment_spatial'): + # if out_shape=="INPUT": + # _out_shape = [1] + list(self.input_res) + # elif out_shape=="SAME": + # _out_shape = + + def _prepare_samples(self, *samples_raw, scale_range=(1.,1.), rotation_mode="90", crop_shape="INPUT"): + """ Data augmentation for discriminator input. + + 1. scale image resolution with random scaling factor from scale_range using bilinear interpolation. + 2. appy random rotation + 3. pad the image to be at have least size crop_shape + 4. apply random crop, focusing on the center of mass, if possible + + """ + samples = [] + with self.opt_ctx.profiler.sample('prepare_crop_flip'): + for sample_raw in samples_raw: + sample_shape = shape_list(sample_raw) + #raw shape & target/crop shape -> scale range + #check allowed scale range + #now allow any scale range and pad later if necessary + #scale_range = self._scale_range(sample_shape[-3:-1], crop_shape, scale_range) + if not (scale_range==None or scale_range==(1.,1.)): + scale = np.random.uniform(*scale_range) + if scale!=1.: + scale_shape = [int(np.ceil(_*scale)) for _ in sample_shape[-3:-1]] + sample = tf.image.resize_bilinear(sample_raw, scale_shape) + else: + sample = sample_raw + + #random 90deg rotation and mirroring + if rotation_mode==90 or rotation_mode=="90": + r = np.random.randint(2, size=3) + if r[0]==1: + sample = tf.transpose(sample, (0,2,1,3)) #swap x and y of NHWC tensor + flip_axes = [] + if r[1]==1: + flip_axes.append(-2) #flip x + if r[2]==1: + flip_axes.append(-3) #flip y + if flip_axes: + sample = tf.reverse(sample, flip_axes) + elif rotation_mode.upper()=="CONTINUOUS": + raise NotImplementedError + # angle = np.random.uniform(0,360) + # sample_shape = shape_list(sample) + # sample_shape = [sample_shape[0], 1] + sample_shape[1:] + # sample = tf.reshape(sample, sample_shape) + # t_from = GridTransform(sample_shape[-4:-1], rotation_deg=(0.,0.,angle), center=True) + # t_to = GridTransform(sample_shape[-4:-1], center=True) + # + # sample = tf.squeeze(self.render_ctx.dens_renderer._sample_transform(sample, [t_from], [t_to], fix_scale_center=True), [1,2]) + elif not (rotation_mode is None or rotation_mode.upper()=="NONE"): + raise ValueError("Unknown rotation_mode %s"%rotation_mode) + + if crop_shape is not None: + if crop_shape=="INPUT": + crop_shape = self.input_res + sample_shape = shape_list(sample) #tf.shape(sample).numpy() + + if np.any(np.less(sample_shape[-3:-1], crop_shape)): + sample = tf_pad_to_shape(sample, [-1]+ list(crop_shape) +[-1], allow_larger_dims=True) #, mode="REFLECT") + sample_shape = shape_list(sample) + + # don't crop if shape already matches + if not np.all(np.equal(sample_shape[-3:-1], crop_shape)): + # -> find a "center of mass" and crop around that, with some random offset + # TODO what if sample is empty/all 0? + crop_eps = 1e-4 + if self.center_crop and tf.reduce_mean(sample).numpy()>crop_eps: + # LOG.info("scale shape: %s", scale_shape) + sample_mean_y = tf.reduce_mean(sample, axis=[-3,-1]) #NW + # LOG.info("mean y shape: %s", sample_mean_y.get_shape().as_list()) + coords_x = tf.reshape(tf.range(0, sample_shape[-2], 1,dtype=tf.float32), (1,sample_shape[-2])) #1W + center_x = tf.reduce_sum(coords_x*sample_mean_y, axis=-1)/tf.reduce_sum(sample_mean_y, axis=-1) #N + sample_mean_x = tf.reduce_mean(sample, axis=[-2,-1]) #NH + coords_y = tf.reshape(tf.range(0, sample_shape[-3], 1,dtype=tf.float32), (1,sample_shape[-3])) #1H + center_y = tf.reduce_sum(coords_y*sample_mean_x, axis=-1)/tf.reduce_sum(sample_mean_x, axis=-1) #N + + # get offset s.t. crop is in bounds, centered on center of mass (+ noise) + crop_shape = tf.constant(crop_shape, dtype=tf.int32) #HW + offset_bounds = sample.get_shape()[-3:-1] - crop_shape #2 + offset = tf.stack([center_y, center_x], axis=-1) + tf.random.uniform([sample.get_shape()[0], 2], -20,21, dtype=tf.float32) - tf.cast(crop_shape/2, dtype=tf.float32) #N2 + offset = tf.clip_by_value(tf.cast(offset, dtype=tf.int32), [0,0], offset_bounds) + sample = tf.stack([tf.image.crop_to_bounding_box(s, *o, *crop_shape) for s, o in zip(sample, offset)], axis=0) + else: + sample = tf.random_crop(sample, [sample_shape[0]]+list(crop_shape)+[sample_shape[-1]]) + + samples.append(sample) + return samples if len(samples)>1 else samples[0] + + def augment_intensity(self, samples, scale_range, gamma_range): + with self.opt_ctx.profiler.sample('augment_intensity'): + scale_shape = (shape_list(samples)[0],1,1,1) + scale = tf.random.uniform(scale_shape, *scale_range, dtype=samples.dtype) + gamma = tf.random.uniform(scale_shape, *gamma_range, dtype=samples.dtype) + scale = [scale,scale,scale] + gamma = [gamma,gamma,gamma] + if self._conditional_hull: #do not scale the intensity of the hull, the disc should be invariant to intensities + scale.append(tf.ones(scale_shape, dtype=samples.dtype)) + gamma.append(tf.ones(scale_shape, dtype=samples.dtype)) + if self._temporal_input: + scale *=3 + gamma *=3 + samples = tf.pow(tf.multiply(samples, tf.concat(scale, axis=-1)), tf.concat(gamma, axis=-1)) + return samples + + def real_samples(self, spatial_augment=True, intensity_augment=True): + with self.opt_ctx.profiler.sample('real_samples'): + with self.opt_ctx.profiler.sample('get data'): + samples = self.real_data.get_next() + samples = self._scale_samples_to_input_res(samples)[0] + if spatial_augment: + samples = self._prepare_samples(*tf.split(samples, samples.get_shape()[0], axis=0), \ + crop_shape="INPUT" if self.scale_to_input_res else self.crop_size, scale_range=self.scale_range, rotation_mode=self.rotation_mode) + else: + samples = self._pad_samples_to_input_res(*tf.split(samples, samples.get_shape()[0], axis=0)) + if intensity_augment: + samples = self.augment_intensity(tf.concat(samples, axis=0), self.opt_ctx.setup.data.discriminator.scale_real, self.opt_ctx.setup.data.discriminator.gamma_real) + # scale_shape = (self.opt_ctx.setup.training.discriminator.num_real,1,1,1) + # scale = tf.random.uniform(scale_shape, *self.opt_ctx.setup.data.discriminator.scale_real, dtype=tf.float32) + # gamma = tf.random.uniform(scale_shape, *self.opt_ctx.setup.data.discriminator.gamma_real, dtype=tf.float32) + # scale = [scale,scale,scale] + # gamma = [gamma,gamma,gamma] + # if self._conditional_hull: #do not scale the intensity of the hull, the disc should be invariant to intensities + # scale.append(tf.ones(scale_shape, dtype=tf.float32)) + # gamma.append(tf.ones(scale_shape, dtype=tf.float32)) + # if self._temporal_input: + # scale *=3 + # gamma *=3 + # samples = tf.pow(tf.multiply(samples, tf.concat(scale, axis=-1)), tf.concat(gamma, axis=-1)) + return samples + + def _render_fake_samples(self, state, name="render_fake_samples"): + dens_transform = state.get_density_transform() + #LOG.debug("Render fake samples '%s' with jitter %s", name, [_.jitter for _ in self.render_ctx.cameras]) + imgs_fake = self.render_ctx.dens_renderer.render_density(dens_transform, self.render_ctx.lights, self.render_ctx.cameras, monochrome=self.render_ctx.monochrome, custom_ops=self.opt_ctx.render_ops) # [NHWC]*V + #imgs_fake = [self.check_input(_, name="%s_render_%04d"%(name, i)) for _, i in zip(imgs_fake, range(len(imgs_fake)))] + if self._conditional_hull: + imgs_hull = self.render_ctx.dens_renderer.project_hull(state.hull, dens_transform, self.render_ctx.cameras) #NDWC + imgs_hull = tf.split(imgs_hull, len(self.render_ctx.cameras), axis=0) #[1DWC]*N + imgs_fake = [tf.concat([f,h], axis=-1) for f,h in zip(imgs_fake, imgs_hull)] + return imgs_fake + + def fake_samples(self, state, history_samples=True, concat=True, spatial_augment=True, intensity_augment=False, name="fake_samples"): + with self.opt_ctx.profiler.sample('fake_samples'): + #prepare fake samples + in_fake = [] + if state is not None: + self.render_ctx.randomize_camera_rotation(x_range=self.cam_x_range, z_range=[0,0]) + + # + + with self.opt_ctx.profiler.sample('render fake'): + # TODO temporal disc input: + if self._temporal_input: + cur_idx = 1 + tmp_fake = [None]*3 + with NO_CONTEXT() if self.opt_ctx.tape is None else self.opt_ctx.tape.stop_recording(): # don't need gradients for cmp images (and probably don't have memory for it...) + #TODO random step. consider data/reconstuction step (i.e. frame skipping) vs dataset steps? + # for testing, use fixed prev/next step + #TODO border handling. use border image in disc triplet? also randomly for other frames/states? + # curr: needs at least 3 frame sequence or will break + # or black inputs. TODO needs black prev/next in real data. + # use fixed random camera transform from current disc input + if state.prev is None: + tmp_fake[1] = self._render_fake_samples(state.next, name=name + "_next") + tmp_fake[2] = self._render_fake_samples(state.next.next, name=name + "_nnext") + cur_idx = 0 + elif state.next is None: + tmp_fake[0] = self._render_fake_samples(state.prev.prev, name=name + "_pprev") + tmp_fake[1] = self._render_fake_samples(state.prev, name=name + "_prev") + cur_idx = 2 + else: + tmp_fake[0] = self._render_fake_samples(state.prev, name=name + "_prev") + tmp_fake[2] = self._render_fake_samples(state.next, name=name + "_next") + cur_idx = 1 + LOG.debug("Render temporal fake disc input '%s', current idx %d. tape available: %s", name, cur_idx, self.opt_ctx.tape is not None) + + imgs_fake = self._render_fake_samples(state, name=name) + + if self._temporal_input: + tmp_fake[cur_idx] = imgs_fake + imgs_fake = [tf.concat(_, axis=-1) for _ in zip(*tmp_fake)] + in_fake += imgs_fake + # dens_transform = state.get_density_transform() + # #LOG.debug("Render fake samples '%s' with jitter %s", name, [_.jitter for _ in self.render_ctx.cameras]) + # imgs_fake = self.render_ctx.dens_renderer.render_density(dens_transform, self.render_ctx.lights, self.render_ctx.cameras, monochrome=self.render_ctx.monochrome, custom_ops=self.opt_ctx.render_ops) #[1DWC]*N + # imgs_fake = [self.check_input(_, name="%s_render_%04d"%(name, i)) for _, i in zip(imgs_fake, range(len(imgs_fake)))] + # if self._conditional_hull: + # imgs_hull = self.render_ctx.dens_renderer.project_hull(state.hull, dens_transform, self.render_ctx.cameras) #NDWC + # imgs_hull = tf.split(imgs_hull, len(self.render_ctx.cameras), axis=0) #[1DWC]*N + # in_fake += [tf.concat([f,h], axis=-1) for f,h in zip(imgs_fake, imgs_hull)] + # else: + # in_fake += imgs_fake + #if disc_dump_samples: self.render_ctx.dens_renderer.write_images([tf.concat(disc_in_fake, axis=0)], ['zz_disc_{1:04d}_fake_render_cam{0}'], base_path=setup.paths.data, use_batch_id=True, frame_id=it, format='PNG') + with NO_CONTEXT() if self.opt_ctx.tape is None else self.opt_ctx.tape.stop_recording(): + if self.record_history: + if history_samples: + in_history = self.history.get_samples(self.opt_ctx.setup.training.discriminator.history.samples, replace=False, allow_partial=True) + with tf.device(self.resource_device): #copy to resource device + hist_samples = [tf.identity(_) for batch in in_fake for _ in tf.split(batch, shape_list(batch)[0], axis=0)] + self.history.push_samples(hist_samples, self.opt_ctx.setup.training.discriminator.history.keep_chance, 'RAND') + if history_samples: + in_fake += in_history + #if disc_dump_samples and len(disc_in_history)>0: self.render_ctx.dens_renderer.write_images([tf.concat(disc_in_history, axis=0)], ['zz_disc_{1:04d}_fake_history{0}'], base_path=setup.paths.data, use_batch_id=True, frame_id=it, format='PNG') + if self.opt_ctx.record_summary: + summary_names = self.opt_ctx.make_summary_names('discriminator/history_size') + self.opt_ctx._tf_summary.scalar(summary_names[0], len(self.history), step=self.opt_ctx.iteration) + + in_fake = self._scale_samples_to_input_res(*in_fake) + if spatial_augment: + in_fake = self._prepare_samples(*in_fake, crop_shape="INPUT" if self.scale_to_input_res else self.crop_size, scale_range=self.scale_range, rotation_mode=self.rotation_mode) + else: + in_fake = self._pad_samples_to_input_res(*in_fake) + + if intensity_augment: + raise NotImplementedError + # samples = self.augment_intensity(samples, self.opt_ctx.setup.data.discriminator.scale_fake, self.opt_ctx.setup.data.discriminator.gamma_fake) + return tf.concat(in_fake, axis=0) if concat else in_fake + + def postprocess_loss(self, loss, out, name="loss"): + self.opt_ctx.add_loss(tf.math.reduce_mean(loss), loss_name='discriminator/'+name) + if self.opt_ctx.setup.training.discriminator.use_fc: + scores = tf.math.sigmoid(out) + else: + scores = tf.reduce_mean(tf.math.sigmoid(out), axis=[1,2,3]) + return scores + + def loss(self, input, flip_target=False, training=True): + ''' + Relativistic discriminator: https://github.com/AlexiaJM/relativistic-f-divergences + input: (real, fake), (input) for SGAN + flip_target: + ''' + name = "fake_loss" if flip_target else "real_loss" + if self.loss_type=="SGAN": + if flip_target: + out = self.model(self.check_input(input[0], "fake"), training=training) + loss = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(logits=out, labels=self.fake_labels(out))) + #return loss_disc_fake(self, in_fake, training) + else: + out = self.model(self.check_input(input[0], "real"), training=training) + loss = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(logits=out, labels=self.real_labels(out))) + #return loss_disc_real(self, in_real, training) + + if self.opt_ctx.setup.training.discriminator.use_fc: + scores = tf.math.sigmoid(out) + else: + scores = tf.reduce_mean(tf.math.sigmoid(out), axis=[1,2,3]) + else: + out_real = self.model(self.check_input(input[0], "real"), training=training) + out_fake = self.model(self.check_input(input[1], "fake"), training=training) + if self.loss_type in ["RpSGAN", "RpLSGAN"]: + #relativistic paired + #batch and (disc out) resolution of fake and real must match here + if flip_target: + out_rel = out_fake-out_real + else: + out_rel = out_real-out_fake + + if self.loss_type=="RpSGAN": + loss = 2*tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(logits=out_rel, labels=self.real_labels(out_rel))) + if self.opt_ctx.setup.training.discriminator.use_fc: + scores = tf.math.sigmoid(out) + else: + scores = tf.reduce_mean(tf.math.sigmoid(out), axis=[1,2,3]) + elif self.loss_type=="RpLSGAN": + loss = 2*tf.reduce_mean(tf.math.squared_difference(out_rel, self._label_real)) + if self.opt_ctx.setup.training.discriminator.use_fc: + scores = out_rel + else: + scores = tf.reduce_mean(out_rel, axis=[1,2,3]) + out = out_rel + + + elif self.loss_type in ["RaSGAN", "RaLSGAN"]: + # relativistic average. patch gan/disc: cmp to average value of every patch + if flip_target: + out_rel_real = out_fake-tf.reduce_mean(out_real)#, axis=0) + out_rel_fake = out_real-tf.reduce_mean(out_fake)#, axis=0) + else: + out_rel_real = out_real-tf.reduce_mean(out_fake)#, axis=0) + out_rel_fake = out_fake-tf.reduce_mean(out_real)#, axis=0) + + if self.loss_type=="RaSGAN": + loss = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(logits=out_rel_real, labels=self.real_labels(out_rel_real))) \ + + tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(logits=out_rel_fake, labels=self.fake_labels(out_rel_fake))) + elif self.loss_type=="RaLSGAN": + loss = tf.reduce_mean(tf.math.squared_difference(out_rel_real, self._label_real)) \ + + tf.reduce_mean(tf.math.squared_difference(out_rel_fake, -self._label_real)) + + out = (out_rel_real, out_rel_fake) + scores = tf.zeros([1], dtype=tf.float32) + + return loss, scores#, name + +def loss_disc_real(disc, in_real, training=True): + in_real = disc.check_input(in_real, "real") + out_real = disc.model(in_real, training=training) + #labels_real = tf.concat([tf.ones([setup.training.discriminator.num_real] + disc_out_real.get_shape().as_list()[1:])*setup.training.discriminator.target_label], axis=0) + loss_real = tf.nn.sigmoid_cross_entropy_with_logits(logits=out_real, labels=disc.real_labels(out_real)) + disc.check_output(out_real, loss_real, in_real, "real") + loss_real = tf.math.reduce_mean(loss_real) + disc.opt_ctx.add_loss([loss_real], loss_value_scaled=loss_real, loss_name='discriminator/real_loss') + if disc.opt_ctx.setup.training.discriminator.use_fc: + scores_real = tf.math.sigmoid(out_real) + else: + scores_real = tf.reduce_mean(tf.math.sigmoid(out_real), axis=[1,2,3]) + return scores_real + +def loss_disc_fake(disc, in_fake, training=True): + in_fake = disc.check_input(in_fake, "fake") + out_fake = disc.model(in_fake, training=training) + #labels_fake = tf.concat([tf.zeros([len(disc_in_fake)] + disc_out_fake.get_shape().as_list()[1:])], axis=0) + loss_fake = tf.nn.sigmoid_cross_entropy_with_logits(logits=out_fake, labels=disc.fake_labels(out_fake)) + disc.check_output(out_fake, loss_fake, in_fake, "fake") + loss_fake = tf.math.reduce_mean(loss_fake) + disc.opt_ctx.add_loss([loss_fake], loss_value_scaled=loss_fake, loss_name='discriminator/fake_loss') + if disc.opt_ctx.setup.training.discriminator.use_fc: + scores_fake = tf.math.sigmoid(out_fake) + else: + scores_fake = tf.reduce_mean(tf.math.sigmoid(out_fake), axis=[1,2,3]) + return scores_fake + +def loss_disc_weights(disc): + loss_scale = disc.opt_ctx.loss_schedules.discriminator_regularization(disc.opt_ctx.iteration) + if disc.opt_ctx.LA(loss_scale): + with disc.opt_ctx.profiler.sample('discriminator regularization'): + disc_weights = disc.var_list() + tmp_loss = tf.reduce_mean([tf.reduce_mean(tf.nn.l2_loss(var)) for var in disc_weights]) + tmp_loss_scaled = tmp_loss * loss_scale + disc.opt_ctx.add_loss([tmp_loss_scaled], tmp_loss, tmp_loss_scaled, loss_scale, 'discriminator/regularization') + return True + return False + +def optStep_discriminator(disc_ctx, state=None, additional_fake_samples=None): + if disc_ctx.train and disc_ctx.opt_ctx.setup.training.discriminator.start_delay<=disc_ctx.opt_ctx.iteration: + LOG.debug("Optimization step for discriminator") + with disc_ctx.opt_ctx.profiler.sample('optStep_discriminator'): + #prepare real samples + disc_in_real = disc_ctx.real_samples(spatial_augment=True, intensity_augment=True) + disc_ctx.dump_samples(disc_in_real, True) + + if disc_ctx.loss_type in ["SGAN"]: + with disc_ctx.opt_ctx.profiler.sample('real'): + with tf.GradientTape() as disc_tape: + disc_loss_real, disc_scores_real = disc_ctx.loss((disc_in_real,), flip_target=False, training=True) #disc_scores_real = loss_disc_real(disc_ctx, disc_in_real) + disc_ctx.opt_ctx.add_loss(disc_loss_real, loss_value_scaled=reduce_losses(disc_loss_real), loss_name='discriminator/loss_real') + loss_disc_weights(disc_ctx) + disc_loss_real = disc_ctx.opt_ctx.pop_losses() + grads = disc_tape.gradient(disc_loss_real, disc_ctx.var_list()) + disc_ctx.optimizer.apply_gradients(zip(grads, disc_ctx.var_list())) + + #prepare fake samples + disc_in_fake = [] + if additional_fake_samples is not None: + r = np.random.choice(len(additional_fake_samples), len(disc_ctx.render_ctx.cameras), replace=False) + disc_in_fake.extend([additional_fake_samples[_] for _ in r]) + disc_in_fake.extend(disc_ctx.fake_samples(state, concat=False, spatial_augment=False, name="disc_fake_samples")) + if disc_ctx.crop_size is not None or disc_ctx.scale_to_input_res: + disc_in_fake = disc_ctx._prepare_samples(*disc_in_fake, crop_shape="INPUT" if disc_ctx.scale_to_input_res else disc_ctx.crop_size, scale_range=disc_ctx.scale_range, rotation_mode=disc_ctx.rotation_mode) + + with disc_ctx.opt_ctx.profiler.sample('fake'): + disc_in_fake = disc_ctx.augment_intensity(tf.concat(disc_in_fake, axis=0), disc_ctx.opt_ctx.setup.data.discriminator.scale_fake, disc_ctx.opt_ctx.setup.data.discriminator.gamma_fake) + # scale_shape = (len(disc_in_fake),1,1,1) + # scale = tf.random.uniform(scale_shape, *disc_ctx.opt_ctx.setup.data.discriminator.scale_fake, dtype=tf.float32) + # gamma = tf.random.uniform(scale_shape, *disc_ctx.opt_ctx.setup.data.discriminator.gamma_fake, dtype=tf.float32) + # scale = [scale,scale,scale] + # gamma = [gamma,gamma,gamma] + # if disc_ctx._conditional_hull: #do not scale the intensity of the hull, the disc should be invariant to intensities + # scale.append(tf.ones(scale_shape, dtype=tf.float32)) + # gamma.append(tf.ones(scale_shape, dtype=tf.float32)) + # if disc_ctx._temporal_input: + # scale *=3 + # gamma *=3 + # disc_in_fake = tf.pow(tf.multiply(tf.concat(disc_in_fake, axis=0), tf.concat(scale, axis=-1)), tf.concat(gamma, axis=-1)) + disc_ctx.dump_samples(disc_in_fake, False) + if disc_ctx.loss_type in ["SGAN"]: + with tf.GradientTape() as disc_tape: + disc_loss_fake, disc_scores_fake = disc_ctx.loss((disc_in_fake,), flip_target=True, training=True) #disc_scores_fake = loss_disc_fake(disc_ctx, disc_in_fake) + disc_ctx.opt_ctx.add_loss(disc_loss_fake, loss_value_scaled=reduce_losses(disc_loss_fake), loss_name='discriminator/loss_fake') + loss_disc_weights(disc_ctx) + disc_loss_fake = disc_ctx.opt_ctx.pop_losses() + grads = disc_tape.gradient(disc_loss_fake, disc_ctx.var_list()) + disc_ctx.optimizer.apply_gradients(zip(grads, disc_ctx.var_list())) + + if not (disc_ctx.loss_type in ["SGAN"]): + with disc_ctx.opt_ctx.profiler.sample(disc_ctx.loss_type): + with tf.GradientTape() as disc_tape: + disc_loss, disc_scores = disc_ctx.loss((disc_in_real, disc_in_fake), False, True) #disc_scores_fake = loss_disc_fake(disc_ctx, disc_in_fake) + disc_ctx.opt_ctx.add_loss(disc_loss, loss_value_scaled=reduce_losses(disc_loss), loss_name='discriminator/'+disc_ctx.loss_type) + loss_disc_weights(disc_ctx) + disc_loss = disc_ctx.opt_ctx.pop_losses() + grads = disc_tape.gradient(disc_loss, disc_ctx.var_list()) + disc_ctx.optimizer.apply_gradients(zip(grads, disc_ctx.var_list())) + + if (disc_ctx.loss_type in ["SGAN"]): + if disc_ctx.opt_ctx.record_summary: + summary_names = disc_ctx.opt_ctx.make_summary_names('discriminator/real_score') + disc_ctx.opt_ctx._tf_summary.scalar(summary_names[0], tf.reduce_mean(disc_scores_real), step=disc_ctx.opt_ctx.iteration) + summary_names = disc_ctx.opt_ctx.make_summary_names('discriminator/fake_score') + disc_ctx.opt_ctx._tf_summary.scalar(summary_names[0], tf.reduce_mean(disc_scores_fake), step=disc_ctx.opt_ctx.iteration) + return disc_loss_real, disc_loss_fake, disc_scores_real, disc_scores_fake + else: + if disc_ctx.opt_ctx.record_summary: + summary_names = disc_ctx.opt_ctx.make_summary_names('discriminator/score') + disc_ctx.opt_ctx._tf_summary.scalar(summary_names[0], tf.reduce_mean(disc_scores), step=disc_ctx.opt_ctx.iteration) + return disc_loss[0], disc_scores + else: + LOG.debug("Optimization discriminator inactive") + return 0,0,0,0 diff --git a/phitest/render/profiling.py b/phitest/render/profiling.py new file mode 100644 index 0000000..1ccfcc2 --- /dev/null +++ b/phitest/render/profiling.py @@ -0,0 +1,393 @@ +from contextlib import contextmanager +from collections import deque +import numpy as np +import time, sys, json + +ENABLE_TENSORFLOW = False +if ENABLE_TENSORFLOW: + try: + import tensorflow as tf + except ModuleNotFoundError: + ENABLE_TENSORFLOW = False + +DEFAULT_STATS_MODE = "WELFORD" + + +#https://stackoverflow.com/questions/27779677/how-to-format-elapsed-time-from-seconds-to-hours-minutes-seconds-and-milliseco +def format_time(t): + '''t (float): time in seconds''' + h, r = divmod(t, 3600) + m, s = divmod(r, 60) + return '{:02d}:{:02d}:{:06.3f}'.format(int(h), int(m), s) + +def time_unit(t, m=1000.): + '''t (float): time in seconds''' + units = ['ns', 'us', 'ms', 's', 'm', 'h', 'd', 'y'] + x = [1e-9, 1e-6, 1e-3, 1.0, 60.0, 3600.0, 3600.0*24.0, 3600.0*24.0*365.0] + for d, u in zip(x, units): + if t/d < m or u==units[-1]: + return '{:.3f} {:<2}'.format(t/d, u) + +class Profiler: + class Sample: + def __init__(self, name, parent, group=None): + self.name = name + self.parent = parent + self.group = group + self.children = {} + self.samples = [] + self.start = time.time() + def add_sample(self, sample): + self.samples.append(sample) + def begin(self): + self.start = time.time() + def end(self): + self.add_sample(time.time()-self.start) + def get_child(self, name, group=None): + if not name in self.children: + self.children[name] = self.__class__(name, self, group=group) + return self.children[name] + @property + def num_samples(self): + return len(self.samples) + def __len__(self): + return self.num_samples + def __getitem__(self, idx): + return self.samples[idx] + @property + def min(self): + return np.amin(self.samples) + @property + def max(self): + return np.amax(self.samples) + @property + def mean(self): + return np.mean(self.samples) + @property + def var(self): + return np.var(self.samples) + @property + def std(self): + return np.std(self.samples) + @property + def sum(self): + return np.sum(self.samples) + + class StreamingSample(Sample): + def __init__(self, name, parent, group=None): + self._min = np.finfo(np.float64).max + self._max = np.finfo(np.float64).min + self._sum = np.float64(0) + self._sum_sq = np.float64(0) + self._num_samples = 0 + self._last_sample = np.float64(0) + super().__init__(name, parent, group=group) + del self.samples + def add_sample(self, sample): + sample = np.float64(sample) + self._min = np.minimum(self._min, sample) + self._max = np.maximum(self._max, sample) + self._sum += sample + self._sum_sq += sample*sample + self._num_samples += 1 + self._last_sample = sample + def __getitem__(self, idx): + if idx==-1: + return self._last_sample + else: + raise IndexError("StreamingSample only keeps the last sample (idx = -1).") + @property + def num_samples(self): + return self._num_samples + @property + def min(self): + return self._min + @property + def max(self): + return self._max + @property + def mean(self): + return np.divide(self._sum, self._num_samples, dtype=np.float64) + @property + def var(self): + mean = self.mean + return (np.divide(self._sum_sq, self._num_samples, dtype=np.float64)) - (mean*mean) + @property + def std(self): + return np.sqrt(self.var) + @property + def sum(self): + return self._sum + + class WelfordOnlineSample(StreamingSample): + #https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Welford's_online_algorithm + def __init__(self, name, parent, group=None): + super().__init__(name, parent, group=group) + del self._sum_sq + self._mean = np.float64(0) + self._M2 = np.float64(0) + def add_sample(self, sample): + sample = np.float64(sample) + self._last_sample = sample + self._min = np.minimum(self._min, sample) + self._max = np.maximum(self._max, sample) + self._sum += sample + #self._sum_sq += sample*sample + + self._num_samples += 1 + delta = sample - self._mean + self._mean += np.divide(delta, self._num_samples, dtype=np.float64) + self._M2 += delta * (sample - self._mean) + @property + def mean(self): + return self._mean + @property + def var(self): + return np.divide(self._M2, self._num_samples, dtype=np.float64) + + # SAMPLE_TYPES = { + # "LIST":Profiler.Sample, + # "STREAMING":Profiler.StreamingSample, + # "WELFORD":Profiler.WelfordOnlineSample, + # } + + def __init__(self, verbose=False, active=True, stats_mode=DEFAULT_STATS_MODE): + stats_mode = stats_mode.upper() + if stats_mode=="STREAMING": + self._root = Profiler.StreamingSample("__root__", None) + elif stats_mode=="LIST": + self._root = Profiler.Sample("__root__", None) + elif stats_mode=="WELFORD": + self._root = Profiler.WelfordOnlineSample("__root__", None) + else: + raise ValueError("Unknown stats_mode '%s'"%stats_mode) + self._current = self._root + self._groups = {} + self.verbose = verbose + self._active = active + + @classmethod + def from_file(cls, path, verbose=False, active=True): + p = cls(verbose, active) + with open(path, 'r') as file: + t = json.load(path) + p.timings = t + return p + + @property + def is_active(self): + return self._active + + def current_sample_path(self): + names = [] + c = self._current + while c!=self._root: + names.append(c.name) + c = c.parent + return "/".join(names[::-1]) + + def _get_group(self, group): + if group not in self._groups: + self._groups[group] = [self._root.get_child(group, group=None), 0] + return self._groups[group] + + def _begin_group(self, group): + if group is None: + return + grp = self._get_group(group) + if grp[1] <= 0: + grp[1] = 0 + grp[0].begin() + grp[1] += 1 + + def _end_group(self, group): + if group is None: + return + grp = self._get_group(group) + grp[1] -= 1 + if grp[1] <= 0: + grp[0].end() + grp[1] = 0 + + def _begin_sample(self, name, group=None): + if not self._active: return + self._current = self._current.get_child(name, group) + self._begin_group(group) + self._current.begin() + + def _end_sample(self, verbose=None): + if not self._active: return + + self._current.end() + if verbose or (self.verbose and verbose is None): + print('\'{}\': {}'.format(self._current.name, time_unit(self._current[-1]))) + self._end_group(self._current.group) + self._current = self._current.parent + + + @contextmanager + def sample(self, name, verbose=None, group=None): + if self._active: + self._begin_sample(name, group=group) + try: + yield # run code to measure + finally: + if self._active: + self._end_sample(verbose) + + if ENABLE_TENSORFLOW: + def begin_gradient_sample(self, x, verbose=None): + @tf.custom_gradient + def f(x): + #print("REGISTER: end gradient sample") + def grad(dy): + self._end_sample(verbose) + #print("END gradient sample") + return tf.identity(dy) + return tf.identity(x), grad + return f(x) + + def end_gradient_sample(self, x, name): + @tf.custom_gradient + def f(x): + #print("REGISTER: begin gradient sample: ", name) + def grad(dy): + self._begin_sample("grad:"+name) + #print("BEGIN gradient sample: ", name) + return tf.identity(dy) + return tf.identity(x), grad + return f(x) + +# +# def start_sample(self, name): +# if name in self.last +# +# def end_sample(self, verbose=False): +# pass + + # for formatting + def _get_max_depth(self, sample, level, level_indent): + depth = 0 + for name in sample.children: + depth = max(depth, level_indent*level + len(name)) + depth = max(depth, self._get_max_depth(sample.children[name], level+1, level_indent)) + return depth + + # for formatting + def _get_indent(self, level_indent, max_indent): + indent = self._get_max_depth(self._root, 0, level_indent) + if max_indent<0: + return indent + return min(max_indent, indent) + + def _print_stats(self, sample, level, t_parent, t_root, file, level_indent, max_indent): + t_remaining = t_parent + for name, current in sorted(sample.children.items(), key=lambda e: e[1].sum, reverse=True): + total_time = current.sum + if level==0: + t_parent = total_time + t_root = total_time + t_remaining -= total_time + s = ' '*level_indent*level+'\'{}\''.format(name) + s = ('{:<'+str(max_indent)+'}').format(s) + file.write('{}: {:>10}, {:10d}, {:>12}, {: 10.05f}, {: 10.05f}, {:>10}, {:>10}, {:>10}\n'.format(s, time_unit(current.mean), current.num_samples, format_time(total_time), 100.*total_time/t_parent, 100.*total_time/t_root, \ + time_unit(current.std), time_unit(current.min), time_unit(current.max))) + self._print_stats(current, level+1, total_time, t_root, file, level_indent, max_indent) + if t_remaining>0.0 and len(sample.children)>0: + s = ' '*level_indent*level+'{}'.format("-") + s = ('{:<'+str(max_indent)+'}').format(s) + file.write('{}: {:>10}, {:10d}, {:>12}, {: 10.05f}, {: 10.05f}\n'.format(s, '-', 0, format_time(t_remaining), 100.*t_remaining/t_parent, 100.*t_remaining/t_root)) + + def stats(self, file=sys.stdout, level_indent=4, max_indent=-1): + if self._active: + max_indent = self._get_indent(level_indent, max_indent) +2 + file.write(('{:<'+str(max_indent)+'}: {:^10}| {:^10}| {:^12}| {:^10}| {:^10}| {:^10}| {:^10}| {:^10}|\n').format('Sample name', "average", "# samples", "total", "% parent", "% root", "std", "min", "max")) + self._print_stats(self._root, 0, 0, 0, file, level_indent, max_indent) + else: + file.write('\nProfiling disabled.\n') + + def save(self, path): + with open(path, 'w') as file: + json.dump(self.timings, path) + +if __name__=='__main__': + #test + print("--- Variance stats tests ---") + samples_low = [4e-3, 6e-4, 1e-3, 45e-3, 8e-4, 1.605e-3] + samples_hight = [0.2, 0.00001, 12, 15983, 2.0400862, 3e-12] + def _test_stats(stats_mode): + p = Profiler(stats_mode=stats_mode) + sample = p._root.get_child("low-var") + for s in samples_low: sample.add_sample(s) + + sample = p._root.get_child("high-var") + for s in samples_hight: sample.add_sample(s) + p.stats() + + for mode in ["LIST", "STREAMING", "WELFORD"]: + print(mode) + _test_stats(mode) + print() + print() + print() + + print("--- Profiler tests ---") + def _test_profiler(p): + with p.sample('total (root)', True): + for i in range(2): + with p.sample('loop 4'): + with p.sample('test 1'): + time.sleep(0.3) + print(p.current_sample_path()) + with p.sample('test 2'): + time.sleep(0.7) + time.sleep(0.12) + #time.sleep(0.11) + for i in range(65): + with p.sample('loop 40'): + time.sleep(0.01) + time.sleep(0.1) + print(p.current_sample_path()) + with p.sample('test (root 2)'): + time.sleep(0.1) + + p_samples = Profiler(stats_mode="LIST") + p_stream = Profiler(stats_mode="STREAMING") + p_welford = Profiler(stats_mode="WELFORD") + + try: + _test_profiler(p_samples) + _test_profiler(p_stream) + _test_profiler(p_welford) + except KeyboardInterrupt: + pass + print() + print("sample-list profiler:") + p_samples.stats() + print() + print("streaming profiler:") + p_stream.stats() + print() + print("welford profiler:") + p_welford.stats() + print() + + def debug_mode(profiler, sample_type): + def debug_sample(sample, sample_type): + print("checking %s... "%(sample.name)) + assert isinstance(sample, sample_type), "is: %s, expected: %s"%(type(sample).__name__, sample_type.__name__) + print("OK") + for name, child in sample.children.items(): + debug_sample(child, sample_type) + debug_sample(profiler._root, sample_type) + + debug_mode(p_welford, Profiler.WelfordOnlineSample) + #debug_mode(p_samples, Profiler.WelfordOnlineSample) + + sys.exit() +else: + DEFAULT_PROFILER = Profiler() + sample = DEFAULT_PROFILER.sample + SAMPLE = sample + stats = DEFAULT_PROFILER.stats + STATS = stats \ No newline at end of file diff --git a/phitest/render/render_helper.py b/phitest/render/render_helper.py new file mode 100644 index 0000000..9112a72 --- /dev/null +++ b/phitest/render/render_helper.py @@ -0,0 +1,116 @@ + +import sys, os, logging, re +import numpy as np +import tensorflow as tf + +LOG = logging.getLogger("Render Helper") + +from .vector import GridShape +import lib.tf_ops + +""" helper functions for various rendering + + +""" + +class ImageWriter: + def __init__(self, renderer, directory, name_proto="img_{id:04d}", format="EXR"): + self.dir = directory + os.makedirs(self.dir, exist_ok=True) + self.name_proto = name_proto + self.format = format + self._id = 0 + + def write_image(images, **kwargs): + raise NotImplementedError + + def write_image_batch(images, **kwargs): + raise NotImplementedError + +def image_volume_slices(data, axis, normalize=True, abs_value=True): + """ split volume along axis + + If a batch dimension (N) is present the returned list + + Args: + data (tf.Tensor): the volume to 'render', with shape DWH, DHWC or NDHWC. + + Returns: + list of tf.Tensor: a list of the volume slices as images, [HWC]. + """ + data_shape = GridShape.from_tensor(data) + data = data_shape.normalize_tensor_shape(data) + + images = [] + for grid in data: + if abs_value: + grid = tf.abs(grid) + if normalize: + grid = grid / tf.reduce_max(grid) + + images.extend(tf.unstack(grid, axis=axis)) + + return images + +def with_border_planes(data, planes=["Z-"], density=1., width=1, offset=0): + """ + + Args: + data (tf.Tensor): the volume to 'render', with shape DWH, DHWC or NDHWC. + planes (list of str): + density (float): + width (int): + + Returns: + tf.Tensor: data with added border planes with shape NDHWC + """ + plane_mask = re.compile("^[XYZxyz][+-]$") + axes = {'X': 3, 'Y': 2, 'Z': 1} + zero_pad = (0,0) + + grid_shape = GridShape.from_tensor(data) + data = grid_shape.normalize_tensor_shape(data) + + for plane in planes: + if plane_mask.search(plane) is None: + raise ValueError + axis = axes[plane.upper()[0]] + plane_shape = grid_shape.value + plane_shape[axis] = width + padding = [zero_pad] * 5 + padding[axis] = (offset,grid_shape[axis]-width-offset) if plane[1]=="-" else (grid_shape[axis]-width-offset,offset) + + data = data + tf.pad(tf.ones(plane_shape, dtype=data.dtype) * density, padding) + + return data + +def render_back_planes(data_transform, lights, cameras, renderer, plane_transforms): + """ + + Args: + data_transform (GridTransform): with tf.tensor in it 'data' attribute + lights (list of Light): + cameras (list of Camera): + renderer (Renderer): + plane_transforms (list of GridTransform): + """ + + # use camera with depth 1 to sample plane + + + +def render_vel_divergence(divergence, transform, cameras, renderer, normalize=True): + raise NotImplementedError + +def render_vel_magnitude_CFL(magnitude, transform, cameras, renderer): + raise NotImplementedError + +def render_vel_abs(vel_centered, transform, cameras, renderer): + raise NotImplementedError + +def render_vel_pos(vel_centered, transform, cameras, renderer): + raise NotImplementedError + +def render_vel_neg(vel_centered, transform, cameras, renderer): + raise NotImplementedError + diff --git a/phitest/render/renderer.py b/phitest/render/renderer.py new file mode 100644 index 0000000..fc36f1f --- /dev/null +++ b/phitest/render/renderer.py @@ -0,0 +1,1130 @@ +import os, copy +from collections.abc import Iterable + + +import numpy as np +import tensorflow as tf + +import imageio +import logging, warnings + +from .profiling import DEFAULT_PROFILER +from .camera import Camera +from .lighting import Light, PointLight +from .transform import GridTransform +from lib.tf_ops import tf_data_gaussDown2D, tf_data_gaussDown3D, shape_list, spatial_shape_list, has_shape, has_rank + +from .vector import GridShape + +from .cuda.ops_loader import sampling_ops, blending_ops, raymarching_ops + + +def format_byte(size): + units = ['B', 'KiB', 'MiB', 'GiB', 'TiB'] #... + size = float(size) + i = 0 + while size > 1024. and i0: + self._setup_static_camera_LuT(transformation, (size, setup_cams), inverse) + + if inverse: luts = [cam.inverseLuT for cam in cams] + else: luts = [cam.LuT for cam in cams] + + return luts + + def check_LoD(self, grid_transform, camera, check_inverse=True, name=None): + _LuT_LoD = self.get_camera_LuT(grid_transform, camera) + shape_list(_LuT_LoD) + axes = [_ for _ in range(len(shape_list(_LuT_LoD))-1)] + LoD_min = tf.reduce_min(_LuT_LoD, axis=axes)[-1].numpy() + LoD_max = tf.reduce_max(_LuT_LoD, axis=axes)[-1].numpy() + del _LuT_LoD + if check_inverse: + _LuT_LoD = self.get_camera_LuT(grid_transform, camera, inverse=True) + shape_list(_LuT_LoD) + axes = [_ for _ in range(len(shape_list(_LuT_LoD))-1)] + LoD_grad_min = tf.reduce_min(_LuT_LoD, axis=axes)[-1].numpy() + LoD_grad_max = tf.reduce_max(_LuT_LoD, axis=axes)[-1].numpy() + del _LuT_LoD + if name is not None: + self.log.info("%s stats: shape: %s (%.2f Mi), step: %f, LoD: %f - %f (grad: %f - %f)", name, camera.transform.grid_size, np.prod(camera.transform.grid_size)/(1024*1024), camera.depth_step, LoD_min, LoD_max, LoD_grad_min, LoD_grad_max) + stats = {"shape":camera.transform.grid_size, "step":camera.depth_step, "LoD_min":LoD_min, "LoD_max":LoD_max, "LoD_grad_min":LoD_grad_min, "LoD_grad_max":LoD_grad_max} + else: + if name is not None: + self.log.info("%s stats: shape: %s (%.2f Mi), step: %f, LoD: %f - %f", name, camera.transform.grid_size, np.prod(camera.transform.grid_size)/(1024*1024), camera.depth_step, LoD_min, LoD_max) + stats = {"shape":camera.transform.grid_size, "step":camera.depth_step, "LoD_min":LoD_min, "LoD_max":LoD_max} + return stats + + # cameras must have the same resolution + def _sample_transform(self, data, from_transformations, to_transformations, inverse=False, fix_scale_center=False, **kernelargs): + ''' + _sample_transform is currently experimental and assumes the output grid to be in a centered [-1,1] cube, so scale input accordingly or use fix_scale_center + ''' + with self.profiler.sample('sample_transform'): + #raise NotImplementedError('TODO: implement non-perspective sampling.') + CM = {False: 'TRANSFORM', True: 'TRANSFORM_REVERSE'} + M = [t.get_transform_matrix().transpose() for t in from_transformations] + if fix_scale_center: + V = [(GridTransform(t.grid_size, scale=[2,2,2], center=True, normalize='ALL').get_transform_matrix()@t.get_inverse_transform()).transpose() for t in to_transformations] + else: + V = [t.get_inverse_transform().transpose() for t in to_transformations] + P = [t.identity_matrix() for t in to_transformations] + F = [[-1,1,-1,1,1,-1] for t in to_transformations] + + if self.sample_gradients: + @tf.custom_gradient + def __sample(x, m, v, p, f, out_shape): #NDHWC, N44, V44, V44, V6, 3(:DHW-out) + with self.profiler.sample('Sampling kernel'): + y = sampling_ops.sample_grid_transform(input=x, matrix_m=m, matrix_v=v, matrix_p=p, frustum_params=f, output_shape=out_shape, \ + interpolation = kernelargs.get("interpolation", self.filter_mode), \ + boundary=kernelargs.get("boundary", self.boundary_mode), \ + mipmapping=kernelargs.get("mipmapping", self.mip_mode), \ + num_mipmaps=self.num_mips, mip_bias=self.mip_bias, coordinate_mode=CM[inverse]) + in_shape = tf.shape(x) + batch = len(m) + views = len(v) + + def grad(dy, variables=None): #NVDHWC + with self.profiler.sample('Sampling gradients kernel'): + #dy_batch = tf.unstack(dy) # N - VDHWC + dx = [] + #for dy_views, m_views in zip(dy_batch, m): #iterate batch (N) + for i in range(batch): #iterate batch (N) + m_views = tf.constant([m[i]]*views, dtype=tf.float32) + dx_views = sampling_ops.sample_grid_transform(input=dy[i], matrix_m=m_views, matrix_v=v, matrix_p=p, frustum_params=f, output_shape=in_shape[-4:-1], \ + interpolation=kernelargs.get("interpolation", self.filter_mode), \ + boundary=kernelargs.get("boundary", self.boundary_mode), \ + mipmapping=kernelargs.get("mipmapping", self.mip_mode), \ + num_mipmaps=self.num_mips, \ + mip_bias=self.mip_bias + self.gradient_mip_bias_add, \ + coordinate_mode=CM[not inverse], separate_camera_batch=False) # V1DHWC + dx.append(tf.reduce_sum(dx_views, axis=0))#1DHWC + if batch>1: + dx = tf.concat(dx, axis=0) #NDHWC + else: + dx = dx[0] + var_grads = [] if variables is None else [None for _ in variables] + if variables is not None: self.log.warning("_sample_transform() called with variables: %s", [(_.name, shape_list(_)) for _ in variables]) + return ([dx, None, None, None, None, None], var_grads) #[dx, None, None, None, None, None] + + return y, grad #NVDHWC, g(NVDHWC)->NDHWC + + with self.profiler.sample('Sampling kernel'): + if self.sample_gradients: + sampled = __sample(data, M, V, P, F, from_transformations[0].grid_size if inverse else to_transformations[0].grid_size) + else: + sampled = sampling_ops.sample_grid_transform(input=data, matrix_m=M, matrix_v=V, matrix_p=P, frustum_params=F, \ + output_shape=from_transformations[0].grid_size if inverse else to_transformations[0].grid_size, \ + interpolation = kernelargs.get("interpolation", self.filter_mode), \ + boundary=kernelargs.get("boundary", self.boundary_mode), \ + mipmapping=kernelargs.get("mipmapping", self.mip_mode), \ + num_mipmaps=self.num_mips, mip_bias=self.mip_bias, \ + coordinate_mode=CM[inverse]) + return sampled #NVDHWC + + + # cameras must have the same resolution + def _sample_camera_transform(self, data, transformations, cameras, inverse=False): + with self.profiler.sample('sample_camera_transform'): + CM = {False: 'TRANSFORM_LINDEPTH', True: 'TRANSFORM_LINDEPTH_REVERSE'} + M = [t.get_transform_matrix().transpose() for t in transformations] + V, P, F = self._get_camera_params_batch(cameras) + + if self.sample_gradients: + @tf.custom_gradient + def __sample(x, m, v, p, f, out_shape): #NDHWC, N44, V44, V44, V6, 3(:DHW-out) + with self.profiler.sample('Sampling kernel'): + y = sampling_ops.sample_grid_transform(input=x, matrix_m=m, matrix_v=v, matrix_p=p, frustum_params=f, output_shape=out_shape, interpolation = self.filter_mode, boundary=self.boundary_mode, \ + mipmapping=self.mip_mode, num_mipmaps=self.num_mips, mip_bias=self.mip_bias, \ + coordinate_mode=CM[inverse]) + in_shape = tf.shape(x) + batch = len(m) + views = len(v) + def grad(dy, variables=None): #NVDHWC + with self.profiler.sample('Sampling gradients kernel'): + dx = [] + for i in range(batch): #iterate batch (N) + with self.profiler.sample(''): + dx_views = sampling_ops.sample_grid_transform(input=dy[i], matrix_m=[m[i]]*views, matrix_v=v, matrix_p=p, frustum_params=f, output_shape=in_shape[-4:-1], interpolation = self.filter_mode, boundary=self.boundary_mode, \ + mipmapping=self.mip_mode, num_mipmaps=self.num_mips, mip_bias=self.mip_bias + self.gradient_mip_bias_add, \ + coordinate_mode=CM[not inverse], separate_camera_batch=False) # V1DHWC + dx.append(tf.reduce_sum(dx_views, axis=0))#1DHWC + if batch>1: + dx = tf.concat(dx, axis=0) #NDHWC + else: + dx = dx[0] + var_grads = [] if variables is None else [None for _ in variables] + if variables is not None: self.log.warning("_sample_camera_transform() called with variables: %s", [(_.name, shape_list(_)) for _ in variables]) + return ([dx, None, None, None, None, None], var_grads) #dx + + return y, grad #NVDHWC, g(NVDHWC)->NDHWC + + with self.profiler.sample('Sampling kernel'): + if self.sample_gradients: + sampled = __sample(data, M, V, P, F, transformations[0].grid_size if inverse else cameras[0].transform.grid_size) + else: + sampled = sampling_ops.sample_grid_transform(input=data, matrix_m=M, matrix_v=V, matrix_p=P, frustum_params=F, output_shape=transformations[0].grid_size if inverse else cameras[0].transform.grid_size, \ + interpolation = self.filter_mode, boundary=self.boundary_mode, mipmapping=self.mip_mode, num_mipmaps=self.num_mips, mip_bias=self.mip_bias, \ + coordinate_mode=CM[inverse]) + return sampled + + # sample transform cameras using cached lookup tables computed from the tranformation matrices + # gives speedup for static MVP - grid setups (static scene and camera, as is common for training) at the cost of memory + def _sample_camera_LuT(self, data, transformation, cameras, inverse=False): + with self.profiler.sample('sample_camera_LuT'): + + if self.sample_gradients: + @tf.custom_gradient + def __sample(x, out_shape): #NDHWC, N44, V44, V44, V6, 3(:DHW-out) + luts = self.__get_cam_luts(transformation, out_shape, cameras, inverse) + with self.profiler.sample('Sampling kernel'): + y = sampling_ops.sample_grid_lut(input=x, lookup=luts, interpolation = self.filter_mode, boundary=self.boundary_mode, \ + mipmapping=self.mip_mode, num_mipmaps=self.num_mips, mip_bias=self.mip_bias, \ + coordinate_mode='LOOKUP', relative_coords=False, normalized_coords=False) + + def grad(dy): #NVDHWC + with self.profiler.sample('Sampling gradients kernel'): + # no batch support here + if tf.shape(dy)[0].numpy() != 1: raise NotImplementedError('camera lut rendering does not support data batches.', tf.shape(dy)[0].numpy()) + dy = dy[0] # N - VDHWC + luts = self.__get_cam_luts(transformation, out_shape, cameras, not inverse) + dx = sampling_ops.sample_grid_lut(input=dy, lookup=luts, interpolation = self.filter_mode, boundary=self.boundary_mode, \ + mipmapping=self.mip_mode, num_mipmaps=self.num_mips, mip_bias=self.mip_bias + self.gradient_mip_bias_add, \ + coordinate_mode='LOOKUP', separate_camera_batch=False, relative_coords=False, normalized_coords=False) # V1DHWC + dx = tf.reduce_sum(dx, axis=0)#1DHWC + return dx #[dx, None, None, None, None, None] + + return y, grad #NVDHWC, g(NVDHWC)->NDHWC + + with self.profiler.sample('Sampling kernel'): + if self.sample_gradients: + sampled = __sample(data, cameras[0].transform.grid_size) + else: + luts = self.__get_cam_luts(transformation, cameras[0].transform.grid_size, cameras, inverse) + sampled = sampling_ops.sample_grid_lut(input=data, lookup=luts, \ + interpolation = self.filter_mode, boundary=self.boundary_mode, mipmapping=self.mip_mode, num_mipmaps=self.num_mips, mip_bias=self.mip_bias, \ + coordinate_mode='LOOKUP', relative_coords=False, normalized_coords=False) + return sampled + + def _raymarch_camera_transform(self, data, transformations, cameras, inverse=False, **kwargs): + if inverse: + if self.blend_mode not in ["ADDITIVE"]: + raise ValueError("Inverse raymarching not possible for blend mode %s"%self.blend_mode) + + with self.profiler.sample('raymarch_camera_transform_inverse'): + with self.profiler.sample('params'): + M = [t.get_transform_matrix().transpose() for t in transformations] + V, P, F = self._get_camera_params_batch(cameras) + grid_size = GridShape(transformations[0].grid_size) + img_shape = GridShape.from_tensor(data) + grid_size.n = img_shape.n + grid_size.c = img_shape.c + with self.profiler.sample('dummy_input'): + dummy_input = tf.zeros(grid_size, dtype=data.dtype) + + with self.profiler.sample('Sampling kernel'): + # use the gradient operation to scatter the targets into the volume. + # since this is the gradient op, input and output refer to the forward op and are reversed here, i.e. output->input. + # contents of 'output_grad' are scattered into the volume, 'input' and 'output' are irrelevant when using ADDITIVE blending. + sampled = raymarching_ops.raymarch_grid_transform_grad(input=dummy_input, output=data, output_grad=data, matrix_m=M, matrix_v=V, matrix_p=P, frustum_params=F, \ + output_shape=[cameras[0].transform.grid_size[0], img_shape.y, img_shape.x], \ + interpolation=kwargs.get("filter_mode", self.filter_mode), \ + boundary=kwargs.get("boundary_mode", self.boundary_mode), \ + blending_mode=kwargs.get("blend_mode", self.blend_mode), \ + separate_camera_batch=kwargs.get("global_sampling", True)) + + else: + with self.profiler.sample('raymarch_camera_transform'): + M = [t.get_transform_matrix().transpose() for t in transformations] + V, P, F = self._get_camera_params_batch(cameras) + # self.log.info("_raymarch_camera_transform transforms:\n\t%s", "\n\t".join(str(_) for _ in transformations)) + # self.log.info("_raymarch_camera_transform matrices:\n\t%s", "\n\t".join(str(_) for _ in M)) + + with self.profiler.sample('Sampling kernel'): + sampled = raymarching_ops.raymarch_grid_transform(input=data, matrix_m=M, matrix_v=V, matrix_p=P, frustum_params=F, \ + output_shape=cameras[0].transform.grid_size, \ + interpolation=kwargs.get("filter_mode", self.filter_mode), \ + boundary=kwargs.get("boundary_mode", self.boundary_mode), \ + blending_mode=kwargs.get("blend_mode", self.blend_mode), \ + separate_camera_batch=kwargs.get("global_sampling", True)) + + #self.log.info("%s raymarch %s -> %s",'inverse' if inverse else 'forward', shape_list(data), shape_list(sampled)) + return sampled + + + def sample_camera(self, data, transformations, cameras, inverse=False, allow_static=True, force_static=False, use_step_channel=None, squeeze_batch=None): + """ + inverse: + False: sample from WorldSpace (defined by transformation) to ViewSpace (definde by cameras) + True: sample from ViewSpace to WorldSpace + squeeze_batch: + True: always remove batch dimension if it is 1 + False: never remove batch dimension + None: remove batch dimension if it is 1 and transformations is no iterable (default) + """ + #check data + #data_shape = tf.shape(data).numpy() + #if not len(data_shape)==5: raise ValueError('data must be 5D (NDHWC)') + data_rank = tf.rank(data).numpy() + if not tf.rank(data).numpy()==5: raise ValueError('data must be 5D (NDHWC), is {}'.format(data_rank)) + + data_shape = GridShape.from_tensor(data) + + #no_batch = False + if not isinstance(transformations, Iterable): + transformations =[transformations] + if data_shape.n!=1: + #raise ValueError('transformation and data batch size mismatch.') + # broadcast single transform to data batch + transformations = transformations * data_shape.n + elif squeeze_batch is None: + squeeze_batch = True + else: + if len(transformations)!=data_shape.n: raise ValueError('transformation and data batch size mismatch: {} - {}'.format(len(transformations), data_shape.n)) + if squeeze_batch is None: + squeeze_batch = False + + # check cameras + if not isinstance(cameras, Iterable): cameras = [cameras] #compat + if len(cameras)>1 and not all((cam.transform.grid_size==cameras[0].transform.grid_size for cam in cameras[1:])): + raise ValueError('all cameras must have the same resolution (DHW). (use Renderer._sort_cameras() for batching.)') + cam_size = cameras[0].transform.grid_size + #check static rendering (precomputed LuT) + sample_lut = False + if allow_static and any((cam.static is not None for cam in cameras)): + if not all((cam.static==transformations[0] for cam in cameras)): + if force_static: raise ValueError('Camera static setup does not match transformation') + else: self.log.warning('Incorrect static camera setup, falling back to transform rendering for static cameras.') + else: + if not no_batch: + if force_static: raise ValueError('Static cameras only work without data batch.') + else: self.log.warning('Incorrect static camera setup, falling back to transform rendering for static cameras.') + else: sample_lut=True + + apply_step_channel = False + if use_step_channel is not None and use_step_channel!=[]: + if np.isscalar(use_step_channel): + data = data * use_step_channel + else: + step_channel = [_%data_shape[-1] for _ in use_step_channel if ((-data_shape[-1]) <= _ and _ < data_shape[-1])] + step_channel = sorted(step_channel) + depth_steps = [cam.depth_step for cam in cameras] + #if: same for every camera, can multiply before sampling (?). should be fine with lerp and grid before is usually smaller + if np.all([step==depth_steps[0] for step in depth_steps]): + # if: every channel is included -> premultiply with scalar + if step_channel==list(range(data_shape[-1])): + data = data * depth_steps[0] + # else -> premultiply with channel vector + else: + data = data * tf.constant([(depth_steps[0] if _ in step_channel else 1) for _ in range(data_shape[-1])], dtype=tf.float32) + #else -> multiply after sampling + else: + apply_step_channel = True + + if sample_lut: + sampled = self._sample_camera_LuT(data, transformations[0], cameras, inverse) + else: + sampled = self._sample_camera_transform(data, transformations, cameras, inverse) #NVDHWC + + + # experimental: depth-step size correction + if apply_step_channel: + shape = shape_list(sampled) + step = tf.constant([[(depth_step if _ in step_channel else no_step) for _ in range(shape[-1])] for depth_step in depth_steps], dtype=tf.float32) #VC + step = tf.reshape(step, (1,shape[-5],1,1,1,shape[-1])) #NVDHWC + shape[-1]=1 + shape[-5]=1 + step = tf.tile(step, shape) + sampled = sampled * tf.stop_gradient(step) + + + return tf.squeeze(sampled, 0) if (squeeze_batch and data_shape.n==1) else sampled + + def raymarch_camera(self, data, transformations, cameras, use_step_channel=None, squeeze_batch=None, **raymarch_kwargs): + #check data + data_rank = tf.rank(data).numpy() + inverse = raymarch_kwargs.get("inverse", False) + if not data_rank==5: raise ValueError('data must be 5D (NDHWC for forward or NVHWC for inverse sampling), is {}'.format(data_rank)) + data_shape = GridShape.from_tensor(data) + + #no_batch = False + if not isinstance(transformations, Iterable): + transformations =[transformations] + if data_shape.n!=1: + # broadcast single transform to data batch + transformations = transformations * data_shape.n + elif squeeze_batch is None: + squeeze_batch = True + if squeeze_batch is None: + squeeze_batch = False + if not inverse: + if len(transformations)!=data_shape.n: raise ValueError('transformation and data batch size mismatch: {} - {}'.format(len(transformations), data_shape.n)) + else: + if len(cameras)!=data_shape[1]: raise ValueError('number of cameras and data view dimension mismatch: {} - {}'.format(len(cameras), data_shape[1])) + + # check cameras + if not isinstance(cameras, Iterable): cameras = [cameras] #compat + if len(cameras)>1 and not all((cam.transform.grid_size==cameras[0].transform.grid_size for cam in cameras[1:])): + raise ValueError('all cameras must have the same resolution (DHW). (use Renderer._sort_cameras() for batching.)') + + if use_step_channel is not None and use_step_channel!=[]: + if np.isscalar(use_step_channel): + data = data * use_step_channel + else: + step_channel = [_%data_shape[-1] for _ in use_step_channel if ((-data_shape[-1]) <= _ and _ < data_shape[-1])] + step_channel = sorted(step_channel) + depth_steps = [cam.depth_step for cam in cameras] + #if: same for every camera, can multiply before sampling (?). should be fine with lerp and grid before is usually smaller + #if np.all([np.isclose(step,depth_steps[0]) for step in depth_steps]): + if np.all([step==depth_steps[0] for step in depth_steps]): + # if: every channel is included -> premultiply with scalar + if step_channel==list(range(data_shape[-1])): + data = data * depth_steps[0] + # else -> premultiply with channel vector + else: + data = data * tf.constant([(depth_steps[0] if _ in step_channel else 1) for _ in range(data_shape[-1])], dtype=tf.float32) + #else -> multiply after sampling + else: + raise ValueError("All cameras must have the same step size for batched rendering:\n%s"%(depth_steps,)) + + sampled = self._raymarch_camera_transform(data, transformations, cameras, **raymarch_kwargs)# NDHWC -> NVHWC (NVHWC -> NDHWC if inverse) + + return tf.squeeze(sampled, 0) if (squeeze_batch and data_shape.n==1) else sampled + + # simple 3D lookup sampling (e.g. for warping) + # lookup coordinates are absolute and not normalized + def _sample_LuT(self, data, luts, combined_batch=False, relative=False, normalized=False, cell_center_offset=0.0): + with self.profiler.sample('sample_LuT'): + luts_shape = GridShape.from_tensor(luts) #shape_list(luts) + if luts_shape.c==3: #[-1]==3: #pad LoD 0 + with self.profiler.sample('auto pad lod'): + lut_pad = [[0,0] for _ in range(len(luts_shape))] + lut_pad[-1][-1] = 1 + luts = tf.pad(luts, lut_pad, "CONSTANT", constant_values=0, name="auto_pad_lut_lod") + luts_shape = GridShape.from_tensor(luts) + if luts_shape.c!=4: + raise ValueError + with self.profiler.sample('Sampling kernel'): + resampled = sampling_ops.sample_grid_lut(input=data, lookup=luts, \ + interpolation = self.filter_mode, boundary=self.boundary_mode, mipmapping=self.mip_mode, num_mipmaps=self.num_mips, mip_bias=self.mip_bias, \ + coordinate_mode='LOOKUP', separate_camera_batch= not combined_batch, cell_center_offset=cell_center_offset, relative_coords=relative, normalized_coords=normalized) + return tf.squeeze(resampled, 1) if combined_batch else resampled + + + def _blend_grid(self, data, blend_mode=None, keep_dims=False): + '''blends the grid along z / depth according to blend mode + + Blend modes: + MAX, MIN, MEAN: the according reduction operations + ADDITIVE: sum-reduction + BEER_LAMBERT: Beer-Lambert without self-attenuation + N.B. returned density is the normal cumulative sum, while the attenuation uses the exlusive cumulative sum. + BEER_LAMBERT_SELF: Beer-Lambert with self-attenuation (i.e. non-exlusive cumulative sum for attenuation) + + Args: + data (tf.Tensor): the light/density grid to blend, shape NDHWC + blend_mode (str): one of MAX, MIN, MEAN, BEER_LAMBERT, BEER_LAMBERT_SELF, ADDITIVE + keep_dims: keeps the depth dimesion int the output. + For Min, MAX, MEAN it is 1. + For BEER_LAMBERT, BEER_LAMBERT_SELF, ADDITIVE it is the original depth D. + Returns: + tf.Tensor: blended grid with shape: NDHWC if keep_dims else NHWC + ''' + with self.profiler.sample('_blend_grid'): + if blend_mode is None: blend_mode = self.blend_mode + + if blend_mode.upper()=='MAX': + return tf.reduce_max(data, axis=-4, keepdims=keep_dims) + elif blend_mode.upper()=='MEAN': + return tf.reduce_mean(data, axis=-4, keepdims=keep_dims) + elif blend_mode.upper()=='MIN': + return tf.reduce_min(data, axis=-4, keepdims=keep_dims) + elif blend_mode.upper()=='BEER_LAMBERT': #BEER_LAMBERT_SELF with cell self-attenuation + grid_shape = GridShape.from_tensor(data) + if grid_shape.c==1: #blending for density only + return tf.math.cumsum(data, axis=-4, exclusive=False) if keep_dims else tf.reduce_sum(data, axis=-4) + + light, density = tf.split(data, [grid_shape.c-1,1], axis=-1) + dens_sum = tf.math.cumsum(density, axis=-4, exclusive=False) + if keep_dims: + return tf.concat([tf.math.cumsum(light * tf.math.exp(-dens_sum), axis=-4, exclusive=False), dens_sum], axis=-1) + else: + return tf.concat([tf.reduce_sum(light * tf.math.exp(-dens_sum), axis=-4), dens_sum[...,-1,:,:,:]], axis=-1) + + elif False: #blend_mode.upper()=='BEER_LAMBERT': # without cell self-attenuation + grid_shape = GridShape.from_tensor(data) + if grid_shape.c==1: #blending for density only + return tf.math.cumsum(data, axis=-4, exclusive=True) if keep_dims else tf.reduce_sum(data, axis=-4) + + light, density = tf.split(data, [grid_shape.c-1,1], axis=-1) + dens_sum = tf.math.cumsum(density, axis=-4, exclusive=True) + if keep_dims: + return tf.concat([tf.math.cumsum(light * tf.math.exp(-dens_sum), axis=-4, exclusive=False), dens_sum], axis=-1) + else: + return tf.concat([tf.reduce_sum(light * tf.math.exp(-dens_sum), axis=-4), tf.reduce_sum(density, axis=-4, keepdims=keep_dims)], axis=-1) + + + @tf.custom_gradient + def __blend(x): + with self.profiler.sample('Blending kernel'): + y = blending_ops.reduce_grid_blend(x, blend_mode, keep_dims) + def grad(dy): + with self.profiler.sample('Blending gradients kernel'): + dx = blending_ops.reduce_grid_blend_grad(dy, y, x, blend_mode, keep_dims) + return dx + return y, grad + + return __blend(data) + + def _tonemap(self, image, mode='NONE', **kwargs): + mode = mode.upper() + if mode=='NONE': + image_sdr = image + elif mode=='CLIP_NEGATIVE': + image_sdr = tf.maximum(image, 0) + elif mode=='SATURATE': + image_sdr = tf.clip_by_value(image, 0, 1) + elif mode=='NORMALIZE': + min = tf.min(image) + if min<0: image -= min + max = tf.max(image) + if max>0: image_sdr = image/max + return image_sdr + + def _apply_custom_ops(self, tensor, custom_ops, name): + result = tensor + if custom_ops is not None and name in custom_ops: + ops = custom_ops[name] + if isinstance(ops, Iterable): + for op in ops: + result = op(result) + else: + result = ops(result) + return result + + #render a batch of cameras with the same size/resolution and scene + def _render_cameras(self, light_density, grid_transform, cameras, camera_size, custom_ops=None): + light_density = self._apply_custom_ops(light_density, custom_ops, "GRID") + if self.can_render_fused and ((custom_ops is None) or ("FRUSTUM" not in custom_ops)): + with self.profiler.sample('Sampling & Reduction'): + image_hdr = self.raymarch_camera(light_density, grid_transform, cameras, use_step_channel=[0,1,2,3] if self.blend_mode not in ['MAX','MEAN','MIN'] else None) + else: + with self.profiler.sample('Sampling'): + frustum_grid = self.sample_camera(light_density, grid_transform, cameras, False, use_step_channel=[0,1,2,3] if self.blend_mode not in ['MAX','MEAN','MIN'] else None) + frustum_grid = self._apply_custom_ops(frustum_grid, custom_ops, "FRUSTUM") + with self.profiler.sample('Reduction'): + image_hdr = self._blend_grid(frustum_grid, self.blend_mode) + image_hdr = self._apply_custom_ops(image_hdr, custom_ops, "IMAGE") + #self.log.info("image shape: %s", shape_list(image_hdr)) + return image_hdr + + def _volume_scatter(self, *, density, light_in, **kwargs): + return density * (self.scattering_ratio * light_in) + + def _build_light_grid(self, density_data, density_transforms, light_list, monochrome=False): + with self.profiler.sample('Lighting'): + self.log.debug('render lights: %d', len(light_list)) + if len(light_list)==0: + #raise ValueError('light list is empty') + self.log.warning('Light list is empty!') + light_shape = GridShape.from_tensor(density_data) #density_transform.grid_shape + light_shape.c = 1 if monochrome else 3 + #light_shape.n = 1 + light_data = tf.zeros(light_shape._value, dtype=tf.float32) + i=0 + for light in light_list: + if isinstance(light, Light): + with self.profiler.sample('Light {}: {}'.format(i, type(light).__name__)): + light_grid = light.grid_lighting(density_data, density_transforms, self) # scattering_func=self._volume_scatter + if monochrome and not light.monochrome: #RGB light to greyscale + light_grid = tf.reduce_sum(light_grid*self.luma, axis=-1, keep_dims=True) + if not monochrome and light.monochrome: #greyscale light to RGB + light_grid = tf.broadcast_to(light_grid, light_shape._value) + elif isinstance(light, (tf.Tensor, np.ndarray)): + ls = GridShape.from_tensor(light) + if ls.c==3 and monochrome: + light = tf.reduce_sum(light*self.luma, axis=-1, keep_dims=True) + light_grid = tf.broadcast_to(light, light_shape._value) + else: + raise ValueError("Type of light {} '{}' is not supported.".format(i, type(light).__name__)) + light_data += light_grid + del light_grid + i+=1 + return light_data + + def render_density(self, density_transform, light_list, camera_list, cut_alpha=True, background=None, monochrome=False, split_cameras=False, custom_ops=None, tonemapping="NONE"): + check_grids = False + """ + density_transform: (list of) GridTransform object that contains (a batch of) the density data + all must have the same grid shape (DHW1) + + returns: list: NHWC image batch for each camera in camera_list. + """ + #verbose = False + with self.profiler.sample('Render'): + ## preprocess density data + if not isinstance(density_transform, Iterable): + density_transforms = [density_transform.copy_no_data() for _ in range(density_transform.get_batch_size())] + density_data = density_transform.data + else: + spatial_shape = density_transform[0].get_grid_size() + density_data = [] + density_transforms = [] + for dens_t in density_transform: + batch_size = dens_t.get_batch_size() + if np.any(np.not_equal(spatial_shape, dens_t.get_grid_size())): + raise ValueError("All density grids must have the same spatial shape for batched rendering.") + density_data.append(dens_t.data) + density_transforms.extend(dens_t.copy_no_data() for _ in range(batch_size)) + #density_transforms + density_data = tf.concat(density_data, axis=0) + del density_transform + + if custom_ops is not None and "DENSITY" in custom_ops: + density_data = self._apply_custom_ops(density_data, custom_ops, "DENSITY") + if check_grids: + if not tf.reduce_all(tf.is_finite(density_data)).numpy(): + self.log.warning("Preprocessed density with shape %s is not finite", shape_list(density_data)) + elif tf.reduce_any(tf.less(density_data, 0.0)).numpy(): + self.log.warning("Preprocessed density with shape %s is negative", shape_list(density_data)) + + ## apply lighting to grid + # color_channel = 1 if monochrome else 3 + light_data = self._build_light_grid(density_data, density_transforms, light_list, monochrome) + if check_grids: + if not tf.reduce_all(tf.is_finite(light_data)).numpy(): + self.log.warning("Light grid with shape %s is not finite", shape_list(light_data)) + elif tf.reduce_any(tf.less(light_data, 0.0)).numpy(): + self.log.warning("Light grid with shape %s is negative", shape_list(light_data)) + self.log.debug('light shape: %s', tf.shape(light_data)) + data = tf.concat([light_data, density_data], axis=-1) + del light_data + del density_data + + ## resample to frustum grid + cam_images = [None]*len(camera_list) + self.log.debug('render cameras: %d', len(camera_list)) + i=0 + with self.profiler.sample('Render Cameras'): + with self.profiler.sample('Sort'): + non_static_cams, static_cams = self._sort_cameras(camera_list) + cameras = non_static_cams + static_cams + for cam_size, cams in cameras: + with self.profiler.sample('Size {} x{}'.format(cam_size, len(cams))): + if not split_cameras: + images = self._render_cameras(data, density_transforms, cams, cam_size, custom_ops=custom_ops) + ts = tf.exp(-images[...,-1:]) + if cut_alpha: + images = images[...,:-1] + images = tf.unstack(images, axis=1) + ts = tf.unstack(ts, axis=1) + + for cam_idx, cam in enumerate(cams): + if split_cameras: + image = self._render_cameras(data, density_transforms, [cam], cam_size, custom_ops=custom_ops) + image = tf.squeeze(image, 1) + t = tf.exp(-image[...,-1:]) + if cut_alpha: + image = image[...,:-1] + #images = tf.concat(images, axis=0) + else: + image = images[cam_idx] + t = ts[cam_idx] + + if check_grids: + if not tf.reduce_all(tf.is_finite(image)).numpy(): + self.log.warning("Raw image of camera %d with shape %s is not finite", cam_idx, shape_list(image)) + elif tf.reduce_any(tf.less(image, 0.0)).numpy(): + self.log.warning("Raw image of camera %d with shape %s is negative", cam_idx, shape_list(image)) + + img_shape = shape_list(image) + if background is not None: + cam_batch_bkg = tf.broadcast_to(background[camera_list.index(cam)], img_shape) + image += cam_batch_bkg * t + with self.profiler.sample('Tonemapping (%s)'%tonemapping): + image = self._tonemap(image, mode=tonemapping) + if cam.scissor_pad is not None: + image = tf.pad(image, [(0,0)] + list(cam.scissor_pad)) + + if check_grids: + if not tf.reduce_all(tf.is_finite(image)).numpy(): + self.log.warning("Postprocessed image of camera %d with shape %s is not finite", cam_idx, shape_list(image)) + elif tf.reduce_any(tf.less(image, 0.0)).numpy(): + self.log.warning("Postprocessed image of camera %d with shape %s is negative", cam_idx, shape_list(image)) + + #reorder rendered images to match order of input cameras + cam_images[camera_list.index(cam)] = image + + if not split_cameras: + del images + #images.append(image_sdr) + i+=1 + #if custom_ops is not None and "DENSITY" in custom_ops: + # density_transform.set_data(t_density) + return cam_images #V - NHWC + + + def render_SDF(self, *args, **kwargs): + raise NotImplementedError + + def render_density_SDF_fused(self, *args, **kwargs): + raise NotImplementedError + + def render_density_SDF_switch(self, *args, render_as_SDF=None, **kwargs): + render_as_SDF = render_as_SDF if render_as_SDF is not None else self.render_as_SDF + if render_as_SDF: + return self.render_SDF(*args, **kwargs) + else: + return self.render_density(*args, **kwargs) + + def resample_grid3D_aligned(self, data, target_shape, align_x='BORDER', align_y='BORDER', align_z='BORDER', allow_split_channels=False): + ''' + align_: alignment for each dimension, string + BORDER: align outer cell borders + CENTER: align outer cell centers + STAGGER_INPUT: align input center to output border + STAGGER_OUTPUT: align input border to output center + N.B.: uses the still somewhat experimental _sample_transform, which uses the render sampling with orthogonal projection. + ''' + in_shape = shape_list(data) #.get_shape().as_list() + assert len(in_shape)== 5, "input shape must be NDHWC, is : {}".format(in_shape) + assert len(target_shape)==3 + in_shape = GridShape.from_tensor(data) + if not allow_split_channels and not in_shape.c in [1,2,4]: raise ValueError("") + # data = in_shape.normalize_tensor_shape(data) + # in_shape = GridShape.spatial_vector + target_shape = GridShape(target_shape) + target_shape.n = in_shape.n + target_shape.c = in_shape.c + # batch = in_shape[0] + batch = in_shape.n + # in_shape = in_shape[-4:-1] + #_sample_transform is currently experimental and assumes the output grid to be in a centered [-1,1] cube, so scale input accordingly + # scale with output shape to get the right offset, depending on alignment for that axis + def get_scale(align, in_size, out_size): + if align.upper() == 'BORDER': + scale = 2./in_size + elif align.upper() == 'CENTER': #scale to align target corner centers, then scale to target border + scale = 2./out_size * (out_size-1.)/(in_size-1.) + elif align.upper() == 'STAGGER_INPUT': #align input center to output border + scale = 2./(in_size-1.) + elif align.upper() == 'STAGGER_OUTPUT': #align input border to output center + scale = 2./(in_size+1.) + else: + raise ValueError("unknown alignment {}".format(align)) + return scale + in_scale = [ + get_scale(align_x, in_shape.x, target_shape.x), + get_scale(align_y, in_shape.y, target_shape.y), + get_scale(align_z, in_shape.z, target_shape.z), + ] + ''' + alignment = [align_z, align_y, align_y] + for dim in range(3): + align = alignment[dim] + if align.upper() == 'BORDER': + scale = 2./in_shape[dim] + elif align.upper() == 'CENTER': + #scale = 2./(in_shape[dim]-1.)*(target_shape[dim]-1.)/target_shape[dim] + #scale to align target corner centers, then scale to target border + scale = 2./target_shape[dim] * (target_shape[dim]-1.)/(in_shape[dim]-1.) + elif align.upper() == 'STAGGER_INPUT': #align input center to output border + scale = 2./(in_shape[dim]-1.) + elif align.upper() == 'STAGGER_OUTPUT': #align input border to output center + #raise NotImplementedError + scale = 2./(in_shape[dim]+1.) + else: + raise ValueError("unknown alignment {} for axis {:d}".format(align, dim)) + in_scale.insert(0,scale) # z,y,x -> x,y,z scale dimensions + ''' + in_transform = GridTransform(in_shape.zyx.value, scale=in_scale, center=True) + # only shape important here + target_transform = GridTransform(target_shape.zyx.value) + if allow_split_channels and not in_shape.c in [1,2,4]: + # channel into batch + data = tf.reshape(data, in_shape.as_shape.tolist() + [1]) #NDHWC1 + data = tf.transpose(data, [0,5,1,2,3,4]) #NCDHW1 + data = tf.reshape(data, [in_shape.n*in_shape.c,in_shape.z,in_shape.y,in_shape.x,1]) #(NC)DHW1 + # sample with single channel + data = tf.squeeze(self._sample_transform(data, [in_transform]*(in_shape.n*in_shape.c), [target_transform]),1) + # extract channel from batch + data = tf.reshape(data, [target_shape.n,target_shape.c,target_shape.z,target_shape.y,target_shape.x,1]) #NCDHW1 + data = tf.transpose(data, [0,2,3,4,1,5]) #NDHWC1 + data = tf.reshape(data, target_shape.as_shape.tolist()) #NDHWC + else: + data = tf.squeeze(self._sample_transform(data, [in_transform]*batch, [target_transform]),1) + return data + + + def resample_grid3D_offset(self, data, offsets, target_shape): + if not isinstance(offsets, tf.Tensor): + offsets = tf.constant(offsets, dtype=tf.float32) + offsets_shape = shape_list(offsets) + if len(offsets_shape)!=2 or offsets_shape[1]!=3: + raise ValueError("Shape of offsets must be (N,3), is %s"%offsets_shape) + if len(target_shape)!=3: + raise ValueError("target_shape must be (3,), is %s"%target_shape) + data_shape = GridShape.from_tensor(data) + + offsets = tf.pad(offsets, ((0,0),(0,1))) #pad to (N,4) + offsets = tf.reshape(offsets, [offsets_shape[0],1,1,1,4]) + offsets = tf.broadcast_to(offsets, [offsets_shape[0]]+target_shape+[4]) + + return self._sample_LuT(data, offsets, relative=True) + + ''' this is broken! might work with CLAMP/WRAP bounds now? + # z-dim == 1 ... + def resample_grid2D_aligned(self, data, target_shape, alignment=['border', 'border']): + in_shape = data.get_shape().as_list() + assert len(in_shape)== 4, "input shape must be NHWC, is : {}".format(in_shape) + assert len(target_shape)==2 + target_shape = list(target_shape) + target_shape.insert(0,1) + in_shape.insert(1,1) + data = tf.reshape(data, in_shape) + alignment = ['border'] + list(alignment) + return tf.squeeze(self.resample_grid3D_aligned(data, target_shape, alignment), 1) + ''' + + def unproject(self, grid_transform, targets, cameras, blend_func=tf.minimum): + #--- Volume Estimation --- + if self.allow_fused and False: + inp = tf.zeros(grid_transform.grid_shape._value, dtype=tf.float32) + M = [grid_transform.get_transform_matrix().transpose()] + V, P, F = self._get_camera_params_batch(cameras) + tar = tf.expand_dims(targets, 0) + # use the gradient operation to scatter the targets into the volume. contents of input and output are irrelevant when using ADDITIVE blending + unprojections = raymarching_ops.raymarch_grid_transform_grad(input=inp, output=tar, output_grad=tar, matrix_m=M, matrix_v=V, matrix_p=P, frustum_params=F, \ + output_shape=[cameras[0].transform.grid_size[0]] + list(targets.shape)[1:3],\ + interpolation=self.filter_mode, boundary=self.boundary_mode, blending_mode="ADDITIVE") + #targets are [0|1], gradients (here the targets) are summed over the cameras. thus, the voxels seen by all cameras should have the value of the number of cameras. + unprojections = (unprojections - (len(cameras)-0.5)) * 2 + else: + unprojections = tf.ones(grid_transform.grid_shape._value, dtype=tf.float32) + for i in range(len(cameras)): + cam = cameras[i] + #expand target to frustum volume (tile along z) + tar = tf.reshape(targets[i], [1,1] + list(targets[i].shape)) + tar = tf.tile(tar, (1,cam.transform.grid_size[0],1,1,1)) + #sample target to shared volume + unprojection = self.sample_camera(tar, grid_transform, cam, inverse=True) + unprojections = blend_func(unprojections, unprojection) + #unprojection = blend_func(unprojections, axis=0) + return unprojections + + def visual_hull(self, grid_transform, targets, cameras, image_blur=0.0, grid_blur=0.0, threshold=0.5, soft_blur=0.0): + + target_hulls = tf_data_gaussDown2D(targets, image_blur, stride=1, channel=1, padding='SAME') if image_blur>0 else targets + #condition = tf.greater_equal(target_hulls, threshold) + #target_hulls = tf.where(condition, tf.ones_like(target_hulls), tf.zeros_like(target_hulls)) + target_hulls = tf.cast(tf.greater_equal(target_hulls, threshold), tf.float32) + + hull = self.unproject(grid_transform, target_hulls, cameras) + + hull = tf_data_gaussDown3D(hull, grid_blur, stride=1, channel=1, padding='SAME') if grid_blur>0 else hull + #condition = tf.greater_equal(hull, threshold) + #hull = tf.where(condition, tf.ones_like(hull), tf.zeros_like(hull)) + hull = tf.cast(tf.greater_equal(hull, threshold), tf.float32) + + if soft_blur>0: + hull = tf_data_gaussDown3D(hull, soft_blur, stride=1, channel=1, padding='SAME') + + # target_hulls = tf.squeeze(self._sample_camera_transform(hull, [grid_transform], cameras), axis=0) + # target_hulls = self._blend_grid(target_hulls, blend_mode='MAX') + target_hulls = self.project_hull(hull, grid_transform, cameras) + + return hull, target_hulls + + def project_hull(self, hull, grid_transform, cameras): + if self.allow_fused and False: + target_hulls = self.raymarch_camera(hull, [grid_transform], cameras, blend_mode="ADDITIVE") + target_hulls = tf.cast(tf.greater_equal(target_hulls, 0.5), tf.float32) + return target_hulls + else: + target_hulls = [] + for camera in cameras: + target_hull = tf.squeeze(self._sample_camera_transform(hull, [grid_transform], [camera]), axis=0) + target_hull = self._blend_grid(target_hull, blend_mode='MAX') + target_hulls.append(target_hull) + return tf.concat(target_hulls, axis=0) + + def write_images(self, image_batches, file_masks, base_path=None, frame_id=None, use_batch_id=False, format='EXR', y_flip=True, gamma=1.0): + formats = {'EXR':'.exr', 'PNG':'.png'} + os.makedirs(base_path, exist_ok=True) + for image_batch, file_mask in zip(image_batches, file_masks): + i=0 + for image in image_batch: + with self.profiler.sample("prep write img"): + image_shape = shape_list(image) + if y_flip:# y-flip + image = tf.reverse(image, axis=[-3]) + if image_shape[-1]==2: + image = tf.pad(image, [[0,0] for _ in range(len(image_shape)-1)] + [[0,1]]) + image_shape = shape_list(image) + if frame_id is not None and use_batch_id: + file_name = file_mask.format(i, frame_id) + elif frame_id is not None and not use_batch_id: + file_name = file_mask.format(frame_id) + elif frame_id is None and use_batch_id: + file_name = file_mask.format(i) + else: + file_name = file_mask + file_name += formats[format] + path = os.path.join(base_path, file_name) if base_path is not None else file_name + #projected_grads = np.flip(projected_grads, axis=0) + if gamma!=1.0: + image = gammaCorrection(image, gamma) + + with self.profiler.sample("write img"): + if format=='EXR': + try: + imageio.imwrite(path, image, 'EXR-FI') + except KeyboardInterrupt: + raise + except: + self.log.exception("Failed to write exr image with shape %s to '%s':", image_shape, path) + return + elif format=='PNG': + image = (np.clip(image, 0.0, 1.0)*255.0).astype(np.uint8) + try: + imageio.imwrite(path, image) + except KeyboardInterrupt: + raise + except: + self.log.exception("Failed to write png image with shape %s to '%s':", image_shape, path) + return + else: + raise ValueError('format not supported') + i+=1 + + def write_images_batch_views(self, image_batches, file_mask, input_format="VNHWC", base_path=None, frame_idx=None, use_view_idx=True, use_batch_idx=True, image_format='EXR', y_flip=True, gamma=1.0): + """ + images_batches: shape VNHWC, as returned by Renderer.render_density + file mask: string containing formatting keys 'batch' 'view' 'idx' + """ + formats = {'EXR':'.exr', 'PNG':'.png'} + assert input_format in ["NVHWC", "VNHWC"] + batch_first = (input_format=="NVHWC") + + os.makedirs(base_path, exist_ok=True) + for v, image_batch in enumerate(image_batches): + for b, image in enumerate(image_batch): + with self.profiler.sample("prep write imgb"): + if not has_rank(image, 3): + if isinstance(image_batches, (tf.Tensor, np.ndarray)): + shape = shape_list(image_batches) + else: + shape = [len(image_batches)] + if isinstance(image_batch, (tf.Tensor, np.ndarray)): + shape += shape_list(image_batch) + else: + shape += [len(image_batch)] + shape_list(image) + raise ValueError("images batches must have rank 5 (NVHWC or VNHWC), is %d (%s): %s"%(tf.rank(image).numpy() +2, input_format, shape)) + image_shape = shape_list(image) + if y_flip:# y-flip + image = tf.reverse(image, axis=[-3]) + if image_shape[-1]==2: + image = tf.pad(image, [[0,0] for _ in range(len(image_shape)-1)] + [[0,1]]) + image_shape = shape_list(image) + if image_shape[-1]>4: + raise ValueError("Channel dimension of images must be <5") + + fmt_dict = {} + if use_batch_idx: + fmt_dict["batch"] = v if batch_first else b + if use_view_idx: + fmt_dict["view"] = b if batch_first else v + if frame_idx is not None: + fmt_dict["idx"] = frame_idx + file_name = file_mask.format(**fmt_dict) + file_name += formats[image_format] + path = os.path.join(base_path, file_name) if base_path is not None else file_name + + #projected_grads = np.flip(projected_grads, axis=0) + if gamma!=1.0: + image = gammaCorrection(image, gamma) + + with self.profiler.sample("write imgb"): + if image_format=='EXR': + try: + imageio.imwrite(path, image, 'EXR-FI') + except KeyboardInterrupt: + raise + except: + self.log.exception("Failed to write exr image with shape %s to '%s':", image_shape, path) + return + elif image_format=='PNG': + image = (np.clip(image, 0.0, 1.0)*255.0).astype(np.uint8) + try: + imageio.imwrite(path, image) + except KeyboardInterrupt: + raise + except: + self.log.exception("Failed to write png image with shape %s to '%s':", image_shape, path) + return + else: + raise ValueError("Unsupported image_format '%s'"%(image_format,)) + \ No newline at end of file diff --git a/phitest/render/serialization.py b/phitest/render/serialization.py new file mode 100644 index 0000000..8f95460 --- /dev/null +++ b/phitest/render/serialization.py @@ -0,0 +1,35 @@ +import numpy as np +import importlib +#import numbers +#https://medium.com/python-pandemonium/json-the-python-way-91aac95d4041 +def to_dict(obj): + if obj is None or isinstance(obj, (int, bool, float, str, list, tuple, dict)): + return obj + if isinstance(obj, (np.ndarray, np.number)): + # alternative: save ndarray as .npz and put path here. might need base path + return obj.tolist() + d = { + "__class__":obj.__class__.__name__, + "__module__":obj.__module__ + } + if hasattr(obj, "to_dict"): + d.update(obj.to_dict()) + else: + d.update(obj.__dict__) + return d + +def from_dict(d): + if d is None or isinstance(d, (int, bool, float, str, list, tuple)): + return d + elif "__class__" in d: + cls_name = d.pop("__class__") + mod_name = d.pop("__module__") + mod = importlib.import_module(mod_name) + cls = getattr(mod, cls_name) + if hasattr(cls, "from_dict"): + obj = cls.from_dict(d) + else: + obj = cls(**d) + else: + obj = d + return obj \ No newline at end of file diff --git a/phitest/render/transform.py b/phitest/render/transform.py new file mode 100644 index 0000000..73d156a --- /dev/null +++ b/phitest/render/transform.py @@ -0,0 +1,428 @@ +import copy +import numpy as np +from scipy.spatial.transform import Rotation +from scipy.stats import special_ortho_group +from .serialization import to_dict, from_dict + +from .vector import * + +from lib.tf_ops import shape_list + +class MatrixTransform(object): + def __init__(self, transform_matrix=None, parent=None, grid_size=None, static=False): + if transform_matrix is not None: + self._matrix = transform_matrix + else: + self._matrix = self.identity_matrix() + if parent is not None and not isinstance(parent, MatrixTransform): + raise TypeError("parent must be a Transform object or None.") + self.parent = parent + self.grid_size=grid_size + @classmethod + def from_dict(cls, d): + p = d.pop("parent") + p = from_dict(p) + return cls(parent=p, **d) + + @classmethod + def from_lookat(cls, eye, lookat, parent=None): + pass + + @classmethod + def from_fwd_up_right_pos(cls, fwd, up, right, pos, parent=None): + mat = np.asarray( + [[right[0],up[0],fwd[0],pos[0]], + [right[1],up[1],fwd[1],pos[1]], + [right[2],up[2],fwd[2],pos[2]], + [0,0,0,1]], + dtype=np.float32) + return cls(mat, parent) + + @classmethod + def from_transform(cls, transform, parent=None): + raise NotImplementedError() + + @staticmethod + def translation_matrix(translation): + return np.asarray( + [[1,0,0,translation[0]], + [0,1,0,translation[1]], + [0,0,1,translation[2]], + [0,0,0,1]], + dtype=np.float32) + @staticmethod + def get_random_rotation(random_state=None): + #return special_ortho_group.rvs(3) #Rotation.from_matrix(special_ortho_group.rvs(3)) needs scipy>=1.4.0 + return Rotation.random(random_state=random_state).as_euler("xyz", degrees=True) + @staticmethod + def rotation_matrix(rotation): + if isinstance(rotation, Rotation): + rot = rotation.as_dcm() + elif shape_list(rotation)==[3,3]: + rot = np.array(rotation) + elif shape_list(rotation)==[3,]: + #rot = Rotation.from_euler('zyx', np.flip(rotation), degrees=True).as_dcm() + rot = Rotation.from_euler('xyz', rotation, degrees=True).as_dcm() + else: + raise ValueError("Can't use %s as rotation"%(rotation,)) + rot = np.pad(rot, (0,1), mode='constant') + rot[-1,-1]=1 + return rot + @staticmethod + def scale_matrix(scale): + return np.asarray( + [[scale[0],0,0,0], + [0,scale[1],0,0], + [0,0,scale[2],0], + [0,0,0,1]], + dtype=np.float32) + @staticmethod + def identity_matrix(): + return np.asarray( + [[1,0,0,0], + [0,1,0,0], + [0,0,1,0], + [0,0,0,1]], + dtype=np.float32) + + def set_parent(self, parent_transform): + if parent_transform is not None and not isinstance(parent_transform, MatrixTransform): + raise TypeError("parent must be a Transform object or None.") + self.parent = parent_transform + + def copy_no_data(self): + return copy.deepcopy(self) + + def get_local_transform(self): + return self._matrix + + def position_global(self): + return self.get_transform_matrix()@np.asarray([0,0,0,1]) + def forward_global(self): + v = self.get_transform_matrix()@np.asarray([0,0,1,0]) + return v/np.linalg.norm(v) + def up_global(self): + v = self.get_transform_matrix()@np.asarray([0,1,0,0]) + return v/np.linalg.norm(v) + def right_global(self): + v = self.get_transform_matrix()@np.asarray([1,0,0,0]) + return v/np.linalg.norm(v) + + def transform(self, vector): + if isinstance(vector, (Vector2,Vector3)): + raise TypeError("use Vector4 for transformations") + elif isinstance(vector, Vector4): + return Vector4(self.get_transform_matrix() @ vector.value) + else: + return self.get_transform_matrix() @ np.asarray(vector) + + def transform_AABB(self, corner_min=[0,0,0], corner_max=[1,1,1], expand_corners=False): + corners = [] + cmin = Vector3(corner_min) + cmax = Vector3(corner_max) + corners.append(self.transform([cmin.x, cmin.y, cmin.z, 1])) + if expand_corners: + corners.append(self.transform([cmax.x,cmin.y,cmin.z, 1])) + corners.append(self.transform([cmin.x,cmax.y,cmin.z, 1])) + corners.append(self.transform([cmin.x,cmin.y,cmax.z, 1])) + corners.append(self.transform([cmax.x,cmax.y,cmin.z, 1])) + corners.append(self.transform([cmax.x,cmin.y,cmax.z, 1])) + corners.append(self.transform([cmin.x,cmax.y,cmax.z, 1])) + corners.append(self.transform([cmax.x,cmax.y,cmax.z, 1])) + return corners + + + def get_transform_matrix(self): + if self.parent is not None: + return self.parent.get_transform_matrix() @ self.get_local_transform() + else: + return self.get_local_transform() + def get_inverse_transform(self): + return np.linalg.inv(self.get_transform_matrix()) + def inverse(self): + return MatrixTransform(self.get_inverse_transform()) #includes parent transform! + def is_static(self): + if not self.static: + return False + elif self.parent is not None: + return self.parent.is_static() + else: + return True + #operators + def __eq__(self, other): + return self.get_transform_matrix() == other.get_transform_matrix() + + def to_dict(self): + return { + "transform_matrix":np.asarray(self._matrix).tolist(), + "parent":to_dict(self.parent), + "grid_size":self.grid_size, + } + +class Transform(MatrixTransform): + def __init__(self, translation=[0,0,0], rotation_deg=[0,0,0], scale=[1,1,1], parent=None, static=False, rotation_rotvec=None, rotation_quat=None): + self.translation = translation + if rotation_quat is not None: + self.rotation_quat = rotation_quat + elif rotation_rotvec is not None: + self.rotation_rotvec = rotation_rotvec + else: + self.rotation_deg = rotation_deg + self.scale = scale + self.parent = parent + @classmethod + def from_dict(cls, d): + p = d.pop("parent") + p = from_dict(p) + return cls(parent=p, **d) + + def set_translation(self, translation): + if translation is None: + self.translation = [0,0,0] + else: + assert isinstance(translation, (list, np.ndarray)) and len(translation)==3 + self.translation = translation + + @property + def rotation_deg(self): + return self._rotation.as_euler("xyz", degrees=True) + @rotation_deg.setter + def rotation_deg(self, value): + assert isinstance(value, (list, np.ndarray)) and len(value)==3 + self._rotation = Rotation.from_euler("xyz", value, degrees=True) + def add_rotation_deg(self, x=0,y=0,z=0): + r = self.rotation_deg + r[0] += x + r[1] += y + r[2] += z + self.rotation_deg = r + def set_rotation_deg(self, x=None,y=None,z=None): + r = self.rotation_deg + if x is not None: + r[0] = x + if y is not None: + r[1] = y + if z is not None: + r[2] = z + self.rotation_deg = r + @property + def rotation_rotvec(self): + return self._rotation.as_rotvec() + @rotation_rotvec.setter + def rotation_rotvec(self, value): + assert isinstance(value, (list, np.ndarray)) and len(value)==3 + self._rotation = Rotation.from_rotvec(value) + @property + def rotation_quat(self): + return self._rotation.as_quat() + @rotation_quat.setter + def rotation_quat(self, value): + assert isinstance(value, (list, np.ndarray)) and len(value)==4 + self._rotation = Rotation.from_quat(value) + + def set_rotation_angle(self, rotation_deg): + if rotation_deg is None: + self.rotation_deg = [0,0,0] + else: + assert isinstance(rotation_deg, (list, np.ndarray)) and len(rotation_deg)==3 + self.rotation_deg = rotation_deg + + def set_scale(self, scale): + if scale is None: + self.scale = [1,1,1] + else: + assert isinstance(scale, (list, np.ndarray)) and len(scale)==3 + self.scale = scale + + def set_rotation_quaternion(self, rotation): + raise NotImplementedError + + def translate_local(self, translation): + raise NotImplementedError + def rotate_around_local(self, axis, angle_deg): + raise NotImplementedError + def scale_local(self, scale): + raise NotImplementedError + + def get_local_transform(self): + M_scale = Transform.scale_matrix(self.scale) + M_rot = Transform.rotation_matrix(self._rotation) + M_trans = Transform.translation_matrix(self.translation) + return M_trans@(M_rot@M_scale) + #operators + def __eq__(self, other): + return self.get_transform_matrix() == other.get_transform_matrix() + def __str__(self): + return '{}: t={}, r={}, s={}; p=({})'.format(type(self).__name__, self.translation, self.rotation_deg, self.scale ,self.parent) + + def to_dict(self): + return { + "translation":list(self.translation), + "rotation_quat":list(self.rotation_quat), + "scale":list(self.scale), + "parent":to_dict(self.parent), + } + +class GridTransform(Transform): + def __init__(self, grid_size, translation=[0,0,0], rotation_deg=[0,0,0], scale=[1,1,1], center=False, normalize='NONE', parent=None, static=False, rotation_rotvec=None, rotation_quat=None): + # center: offset grid s.t. its center is at (0,0,0) is OS + # normalize: normalize size to (1,1,1) with 1/grid-size + super().__init__(translation, rotation_deg=rotation_deg, scale=scale, parent=parent, rotation_rotvec=rotation_rotvec, rotation_quat=rotation_quat) + self.__data=None + self.grid_size=grid_size + self.center = center + self.normalize = normalize + @classmethod + def from_dict(cls, d): + p = d.pop("parent") + p = from_dict(p) + return cls(parent=p, **d) + + @classmethod + def from_transform(cls, transform, grid_size, center=False, normalize='NONE'): + return cls(grid_size, translation=transform.translation, rotation_quat=transform.rotation_quat, scale=transform.scale, center=center, normalize=normalize, parent=transform.parent) + @classmethod + def from_grid(cls, grid, translation=[0,0,0], rotation_deg=[0,0,0], scale=[1,1,1], center=False, normalize='NONE', parent=None): + pass + @classmethod + def from_grid_transform(cls, grid, transform, center=False, normalize='NONE'): + pass + + @property + def grid_size(self): + return self.__grid_shape.spatial_vector.as_shape.tolist() + @grid_size.setter + def grid_size(self, value): + if self.__data is not None: + raise ValueError("Can't set grid_size if GridTransform that has assoziated data set.") + else: + assert isinstance(value, Int3) or shape_list(value)==[3,] + self.__grid_shape = GridShape(value) + + @property + def has_data(self): + return (self.__data is not None) + + @property + def data(self): + return self.__data + @data.setter + def data(self, value): + self.set_data(value) + + def set_data(self, data, format='NDHWC'): #TODO rename: set_grid + if data is None: + self.__data = None + return + assert isinstance(data, (tf.Tensor, np.ndarray)) + #data_shape = data.get_shape().as_list() + #self.grid_size = [data_shape[format.index(_)] for _ in 'DHW'] + self.__data = data + self.__grid_shape = GridShape.from_tensor(self.__data) + def get_grid(self): + return self.data + + def _data_shape(self): + if self.__data is not None: + return GridShape.from_tensor(self.__data) + else: + raise ValueError("data is not set") + + @property + def grid_shape(self): + return self.__grid_shape.copy() + @grid_shape.setter + def grid_shape(self, value): + if self.__data is not None: + raise ValueError("Can't set grid_size if GridTransform that has assoziated data set.") + else: + assert isinstance(value, GridShape) + self.__grid_shape = value.copy() + + def get_grid_size(self): #TODO rename: get_grid_shape + return np.asarray(self.grid_size) + + def get_channel(self): + if self.__data is not None: + #return self.data.get_shape().as_list()[-1] + return self.grid_shape.c + else: raise ValueError("data is not set") + + def get_batch_size(self): + if self.__data is not None: + #return self.data.get_shape().as_list()[0] + return self.grid_shape.n + else: raise ValueError("data is not set") + + def copy_no_data(self): + gt = copy.copy(self) + gt.data = None + return copy.deepcopy(gt) + + def copy_new_data(self, data): + gt = self.copy_no_data() + gt.set_data(data) + return gt + + def copy_same_data(self): + return self.copy_new_data(self.data) + + def get_local_transform(self): + size = np.flip(self.get_grid_size()) # shape is zyx, but coordinates are xyz + M_center = Transform.translation_matrix(-size/2.0) + if self.normalize=='ALL': + M_norm_scale = Transform.scale_matrix(1.0/size) + if self.normalize=='MIN': + M_norm_scale = Transform.scale_matrix(np.asarray([1.0/np.min(size)]*3, dtype=np.float32)) + if self.normalize=='MAX': + M_norm_scale = Transform.scale_matrix(np.asarray([1.0/np.max(size)]*3, dtype=np.float32)) + M_scale = Transform.scale_matrix(self.scale) + M_rot = Transform.rotation_matrix(self._rotation) + M_trans = Transform.translation_matrix(self.translation) + M = M_scale@M_center if self.center else M_scale + M = M_norm_scale@M if self.normalize!='NONE' else M + return M_trans@(M_rot@M) + + def grid_corners_world(self, all_corners=False): + gs = self.grid_shape + return self.transform_AABB(corner_max=[gs.x,gs.y,gs.z], expand_corners=all_corners) + + def grid_size_world(self): + gs = self.grid_shape + dir_x = self.transform(Float4(gs.x,0,0,0)).xyz + dir_y = self.transform(Float4(0,gs.y,0,0)).xyz + dir_z = self.transform(Float4(0,0,gs.z,0)).xyz + return Float3(dir_x.magnitude, dir_y.magnitude, dir_z.magnitude) + + def grid_min_world(self): + return self.transform(Float4(0,0,0,1)).xyz + + def grid_max_world(self): + gs = self.grid_shape + return self.transform(Float4(gs.xyz,1)).xyz + + def cell_size_world(self): + dir_x = self.transform(Float4(1,0,0,0)).xyz + dir_y = self.transform(Float4(0,1,0,0)).xyz + dir_z = self.transform(Float4(0,0,1,0)).xyz + return Float3(dir_x.magnitude, dir_y.magnitude, dir_z.magnitude) + + def __eq__(self, other): + if np.any(np.not_equal(self.get_transform_matrix(), other.get_transform_matrix())): return False + if np.any(np.not_equal(self.get_grid_size(), other.get_grid_size())): return False + #if self.get_grid() != other.get_grid(): return False + return True + def __str__(self): + return '{}: {}{}, t={}{}, r={}, s={}{}; p=({})'.format(type(self).__name__, self.grid_size, 'Y' if self.has_data else 'N', self.translation, 'C' if self.center else '', \ + self.rotation_deg, self.scale, self.normalize if self.normalize!='NONE' else '' ,self.parent) + + def to_dict(self): + return { + "translation":list(self.translation), + "rotation_quat":list(self.rotation_quat), + "scale":list(self.scale), + "center":bool(self.center), + "normalize":str(self.normalize), + "grid_size":list(self.grid_size), + "parent":to_dict(self.parent), + } \ No newline at end of file diff --git a/phitest/render/vector.py b/phitest/render/vector.py new file mode 100644 index 0000000..471366c --- /dev/null +++ b/phitest/render/vector.py @@ -0,0 +1,555 @@ +import tensorflow as tf +import numpy as np +import logging, numbers + +LOG = logging.getLogger("Vector") + + +class Matrix(object): + def __init__(self, *args, **kwargs): + raise NotImplementedError + +class _VectorIterator(object): + def __init__(self, vec, length): + self.__vec = vec + self.__length = length + self.__idx = -1 + + def __iter__(self): + return self + + def __next__(self): + self.__idx +=1 + if self.__idx4: raise AttributeError("At most 4 elements of ({}) can be selected from {}, selection: '{}'".format(", ".join(self._attr_map.keys()), self.__class__.__name__, attr)) + r = [] + for a in attr.lower(): + if a not in self._attr_map: raise AttributeError("Element '{}' from choice '{}' not available for {}, available: ({})".format(a, attr, self.__class__.__name__, ", ".join(self._attr_map.keys()))) + i = self._attr_map[a] + if i>=len(self): raise AttributeError(attr) + r.append(i) + if len(r)==1: return self[r[0]] + elif len(r)==2: return Vector2(self[r]) + elif len(r)==3: return Vector3(self[r]) + elif len(r)==4: return Vector4(self[r])#np.choose(r, self._value)) + # else: + # return super().__getattr__(attr) + + def __setattr__(self, name, value): + if name.lower() in self._attr_map: + if not np.isscalar(value): + raise ValueError("Only scalar values can be assigned to vector elements.") + self._value[self._attr_map[name.lower()]] = value + else: + super().__setattr__(name, value) + + def __array__(self, dtype=None): + if dtype: + return np.array(self._value, dtype=dtype) + else: + return self.value + + def __add__(self, other): + cls = self.__class__ + try: + r = np.add(self, other) + except: + return NotImplemented + return cls(r) + + def __radd__(self, other): + cls = self.__class__ + try: + r = np.add(other, self) + except: + return NotImplemented + return cls(r) + + def __iadd__(self, other): + return self.__add__(other) + + def __sub__(self, other): + cls = self.__class__ + try: + r = np.subtract(self, other) + except: + return NotImplemented + return cls(r) + + def __rsub__(self, other): + cls = self.__class__ + try: + r = np.subtract(other, self) + except: + return NotImplemented + return cls(r) + + def __isub__(self, other): + return self.__sub__(other) + + def __mul__(self, other): + cls = self.__class__ + try: + r = np.multiply(self, other) + except: + return NotImplemented + return cls(r) + + def __rmul__(self, other): + cls = self.__class__ + try: + r = np.multiply(other, self) + except: + return NotImplemented + return cls(r) + + def __imul__(self, other): + return self.__mul__(other) + + def __truediv__(self, other): + cls = self.__class__ + try: + r = np.true_divide(self, other) + except: + return NotImplemented + return cls(r) + + def __rtruediv__(self, other): + cls = self.__class__ + try: + r = np.true_divide(other, self) + except: + return NotImplemented + return cls(r) + + def __itruediv__(self, other): + return self.__truediv__(other) + + def __floordiv__(self, other): + cls = self.__class__ + try: + r = np.floor_divide(self, other) + except: + return NotImplemented + return cls(r) + + def __rfloordiv__(self, other): + cls = self.__class__ + try: + r = np.floor_divide(other, self) + except: + return NotImplemented + return cls(r) + + def __ifloordiv__(self, other): + return self.__floordiv__(other) + + def __mod__(self, other): + cls = self.__class__ + try: + r = np.mod(self, other) + except: + return NotImplemented + return cls(r) + + def __rmod__(self, other): + cls = self.__class__ + try: + r = np.mod(other, self) + except: + return NotImplemented + return cls(r) + + def __imod__(self, other): + return self.__mod__(other) + + def __matmul__(self, other): + cls = self.__class__ + try: + r = np.dot(self, other) + except: + return NotImplemented + return r + + def __rmatmul__(self, other): + cls = self.__class__ + try: + r = np.dot(other, self) + except: + return NotImplemented + return r + + def __imatmul__(self, other): + return self.__matmul__(other) + + def __eq__(self, other): + cls = self.__class__ + if isinstance(other, cls): + if len(self)!=len(other): return False + if (self._value!=other._value).any(): return False + return True + elif isinstance(other, (list, tuple, np.ndarray)): + if len(self)!=len(other): return False + if (self._value!=other).any(): return False + return True + else: + return NotImplemented + + + def __ne__(self, other): + return not self==other + + def __repr__(self): + return "{}: {}".format(self.__class__.__name__, self._value) + + def __deepcopy__(self, *args): + return type(self)(self._value) + + def __copy__(self): + return self.__deepcopy__() + + def copy(self): + return self.__copy__() + + +class GridShape(Vector): + _attr_map = {'n':0,'x':3,'y':2,'z':1,'c':4} + def __init__(self, v): + ''' + v: 3, 4 or 4 element vector: DHW, DHWC, NDHWC. z,y,x order, will be copied + ''' + if isinstance(v, (list, tuple, np.ndarray)): + v = np.array(v, copy=True) + assert v.shape==(3,) or v.shape==(4,) or v.shape==(5,), "Shape: %s"%(v.shape,) + elif isinstance(v, GridShape): + v = v.value + elif isinstance(v, Vector3): + v = v.zyx.value + elif isinstance(v, Vector4): + v = v.zyxw.value + else: + raise ValueError("Unsupported input type") + + if len(v)<4: + v = np.append(v, [1]) + if len(v)<5: + v = np.append([1], v) + + self._value = v.astype(np.int32) + + @classmethod + def from_tensor(cls, tensor): + if isinstance(tensor, (tf.Tensor, tf.Variable)): + shape = tensor.get_shape().as_list() + elif isinstance(tensor, np.ndarray): + shape = list(tensor.shape) + elif isinstance(tensor, (list, tuple)): + shape = list(np.asarray(tensor).shape) + else: + raise TypeError("Creating GridShape from tensor with type '%s' is not supported.", type(tensor).__name__) + return cls(shape) + + def normalize_tensor_shape(self, tensor): + if isinstance(tensor, (tf.Tensor, tf.Variable)): + return tf.reshape(tensor, self._value) + elif isinstance(tensor, np.ndarray): + return np.reshape(tensor, self._value) + else: + raise ValueError("Unsupported tensor type") + + @property + def as_shape(self): + return self.value + + @property + def value(self): + return np.array(self._value, copy=True) + @property + def spatial_vector(self): + return Int3(self.x,self.y,self.z) + + def padding_to(self, shape, offset=[0,0,0,0,0]): + if len(shape)!=5 or len(offset)!=5: + raise ValueError + padding = [] + for i in range(5): + padding.append([offset[i], shape[i] - self[i] - offset[i]]) + return padding + +class Vector2(Vector): + _attr_map = {'x':0,'y':1,'r':0,'g':1} + #def __init__(self, x=0,y=0, *, v=None): + def __init__(self, *args, dtype=None): + ''' + v: 3 element vector with x,y,z order, will be copied + ''' + num_args = len(args) + if num_args==0: + self._value = np.array([0,0], dtype=dtype) + elif num_args==1: + v = args[0] + if np.isscalar(v): + v=[v,v] + if isinstance(v, (list, tuple, np.ndarray)): + self._value = np.array(v, dtype=dtype) + assert self._value.shape==(2,) + elif isinstance(v, Vector2): + self._value = v.__array__(dtype=dtype) + elif isinstance(v, Vector3): + self._value = v.__array__(dtype=dtype)[:-1] + elif isinstance(v, Vector4): + self._value = v.__array__(dtype=dtype)[:-2] + else: + raise ValueError("Unsupported input type") + elif num_args==2: + x,y = args + assert np.isscalar(x) + assert np.isscalar(y) + self._value = np.array([x,y], dtype=dtype) + else: + raise ValueError("Unsupported parameters for Vector2: %s"%args) + + + def __len__(self): + return 2 + @property + def value(self): + return np.array(self._value, copy=True) + + @classmethod + def from_elements(cls,*, x, y): + return cls([x,y]) + + @classmethod + def from_shape(cls, shape): + return cls(shape[::-1]) + +class Float2(Vector2): + def __init__(self, *args): + super().__init__(*args, dtype=np.float32) + +class Int2(Vector2): + def __init__(self, *args): + super().__init__(*args, dtype=np.int32) + +class Vector3(Vector): + _attr_map = {'x':0,'y':1,'z':2,'r':0,'g':1,'b':2} + #def __init__(self, x=0,y=0,z=0, *, v=None): + def __init__(self, *args, dtype=None): + ''' + v: 3 element vector with x,y,z order, will be copied + ''' + num_args = len(args) + if num_args==0: + self._value = np.array([0,0,0], dtype=dtype) + elif num_args==1: + v = args[0] + if np.isscalar(v): + v=[v,v,v] + + if isinstance(v, (list, tuple, np.ndarray)): + self._value = np.array(v, dtype=dtype) + assert self._value.shape==(3,), "Input shape %s must be (3,)"%(self._value.shape,) + elif isinstance(v, Vector3): + self._value = v.__array__(dtype=dtype) + elif isinstance(v, Vector4): + self._value = v.__array__(dtype=dtype)[:-1] + else: + raise ValueError("Unsupported input types: %s"%([type(_).__name__ for _ in args],)) + elif num_args==2: + v1, v2 = args + if isinstance(v1, Vector2) and isinstance(v2, numbers.Number): + self._value = np.array([v1.x,v1.y,v2], dtype=dtype) + elif isinstance(v1, numbers.Number) and isinstance(v2, Vector2): + self._value = np.array([v1,v2.x,v2.y], dtype=dtype) + else: + raise ValueError("Unsupported input types: %s"%([type(_).__name__ for _ in args],)) + elif num_args==3: + x,y,z = args + assert np.isscalar(x) + assert np.isscalar(y) + assert np.isscalar(z) + self._value = np.array([x,y,z], dtype=dtype) + else: + raise ValueError("Unsupported parameters for Vector3: %s"%(args,)) + + + def __len__(self): + return 3 + @property + def value(self): + return np.array(self._value, copy=True) + + @classmethod + def from_elements(cls,*, x, y, z): + return cls([x,y,z]) + + @classmethod + def from_shape(cls, shape): + return cls(shape[::-1]) + +class Float3(Vector3): + def __init__(self, *args): + super().__init__(*args, dtype=np.float32) + +class Int3(Vector3): + def __init__(self, *args): + super().__init__(*args, dtype=np.int32) + +class Vector4(Vector): + _attr_map = {'x':0,'y':1,'z':2,'w':3,'r':0,'g':1,'b':2,'a':3} + #def __init__(self, x=0,y=0,z=0,w=0, *, v=None): + def __init__(self, *args, dtype=None): + ''' + v: 4 element vector with x,y,z,w order, will be copied + ''' + num_args = len(args) + if num_args==1: + v = args[0] + if np.isscalar(v): + v=[v,v,v,v] + if isinstance(v, (list, tuple, np.ndarray)): + self._value = np.array(v, dtype=dtype) + assert self._value.shape==(4,) + elif isinstance(v, Vector3): + self._value = np.append(v.value, [1]) + elif isinstance(v, Vector4): + self._value = v.value + else: + raise ValueError("Unsupported input types: %s"%([type(_).__name__ for _ in args],)) + elif num_args==2: + v1, v2 = args + if isinstance(v1, Vector2) and isinstance(v2, Vector2): + self._value = np.array([v1.x,v1.y,v2.x,v2.y], dtype=dtype) + elif isinstance(v1, Vector3) and isinstance(v2, numbers.Number): + self._value = np.array([v1.x,v1.y,v1.z,v2], dtype=dtype) + elif isinstance(v1, numbers.Number) and isinstance(v2, Vector3): + self._value = np.array([v1,v2.x,v2.y,v2.z], dtype=dtype) + else: + raise ValueError("Unsupported input types: %s"%([type(_).__name__ for _ in args],)) + elif num_args==3: + v1, v2, v3 = args + if isinstance(v1, Vector2) and isinstance(v2, numbers.Number) and isinstance(v3, numbers.Number): + self._value = np.array([v1.x,v1.y,v2,v3], dtype=dtype) + elif isinstance(v1, numbers.Number) and isinstance(v2, Vector2) and isinstance(v3, numbers.Number): + self._value = np.array([v1,v2.x,v2.y,v3], dtype=dtype) + elif isinstance(v1, numbers.Number) and isinstance(v2, numbers.Number) and isinstance(v3, Vector2) : + self._value = np.array([v1,v2,v3.x,v3.y], dtype=dtype) + else: + raise ValueError("Unsupported input types: %s"%([type(_).__name__ for _ in args],)) + elif num_args==4: + x,y,z,w = args + assert np.isscalar(x) + assert np.isscalar(y) + assert np.isscalar(z) + assert np.isscalar(w) + self._value = np.array([x,y,z,w], dtype=dtype) + else: + raise ValueError("Unsupported parameters for Vector4: %s"%(args,)) + + + def __len__(self): + return 4 + @property + def value(self): + return np.array(self._value, copy=True) + + @classmethod + def from_elements(cls,*, x, y, z, w): + return cls([x,y,z,w]) + + @classmethod + def from_shape(cls, shape): + return cls(shape[::-1]) + +class Float4(Vector4): + def __init__(self, *args): + super().__init__(*args, dtype=np.float32) + +class Int4(Vector4): + def __init__(self, *args): + super().__init__(*args, dtype=np.int32) + +""" +class TF_Vector(Vector): + def __init__(self, *args, **kwargs): + raise NotImplementedError + +class TF_Vector3(TF_Vector): + def __init__(self, v): + if isinstance(v, (list, tuple, np.ndarray)): + v = np.array(v, copy=True) + assert v.shape==(3,) + v = tf.constant(v.value) + elif isinstance(v, (tf.Tensor, tf.Variable)): + pass + elif isinstance(v, TF_Vector3): + v = v.value + elif isinstance(v, Vector3): + v = tf.constant(v.value) + else: + raise ValueError("Unsupported input type") + self._value = v +""" \ No newline at end of file diff --git a/reconstruct_sequence.py b/reconstruct_sequence.py new file mode 100644 index 0000000..585c478 --- /dev/null +++ b/reconstruct_sequence.py @@ -0,0 +1,4809 @@ +import os, sys, shutil, socket, faulthandler, signal, math, copy, random, psutil +import datetime, time +import logging, warnings, argparse +import json +import munch, collections.abc +import imageio + + +parser = argparse.ArgumentParser(description='Reconstruct volumetric smoke densities from 2D views.') +parser.add_argument('-s', '--setup', dest='setup_file', default=None, help='setup from JSON file to use') +parser.add_argument('-m', '--model', dest='model', default=None, help='setup from JSON file to use') +parser.add_argument('-d', '--deviceID', dest="cudaID", default="0", help='id of cuda device to use') +parser.add_argument('-r', '--noRender', dest='render', action='store_false', help='turn off final rendering.') +parser.add_argument('--saveVol', dest='save_volume', action='store_true', help='save final volumes.') +parser.add_argument('-f', '--fit', dest='fit', action='store_true', help='run density volume optimization.') +parser.add_argument('-c', '--noConsole', dest='console', action='store_false', help='turn off console output') +parser.add_argument('--debug', dest='debug', action='store_true', help='enable debug output.') +parser.add_argument('--maxMem', dest='max_memory', type=float, default=0.25, help='MiB or fraction of total RAM.') +args = parser.parse_args() + +cudaID = args.cudaID + + +os.environ["CUDA_VISIBLE_DEVICES"]=cudaID +import numpy as np +import tensorflow as tf +tf.enable_eager_execution() +tf.enable_resource_variables() + +from phitest.render import * +import phitest.render.render_helper as render_helper +#from phitest.render.profiling import Profiler +from phitest.render.profiling import DEFAULT_PROFILER #so other modules can import and use SAMPLE without passing on a Profiler() object. +from phitest.render.serialization import to_dict, from_dict +from lib.logger import StreamCapture +from lib.progress_bar import ProgressBar + + +from lib.util import * +from lib.scalar_schedule import * +from lib.tf_ops import * +from lib.data import * +from lib.tf_colormap import * + + + +def get_clip_nearFar(position, focus, depth): + cam_dh = depth*0.5 #depth half + dist = np.linalg.norm(focus-position) + return [dist-cam_dh,dist+cam_dh] + +def build_camera_from_sFcallibration(position, forward, up, right, resolution, fov_horizontal, fov_vertical, focus, focus_depth_clip=1.0, **kwargs): + flip_z = lambda v: np.asarray(v)*np.asarray([1,1,-1]) + invert_v = lambda v: np.asarray(v)*(-1) + pos = flip_z(position) + fwd = invert_v(flip_z(forward)) + up = flip_z(up) + right = flip_z(right) + cam_focus = flip_z(focus) + aspect = fov_horizontal/fov_vertical #resolution[2]/resolution[1] #W/H + cam_dh = focus_depth_clip*0.5 #depth half + + dist = np.linalg.norm(cam_focus-pos) + cam = Camera(MatrixTransform.from_fwd_up_right_pos(fwd, up, right, pos), nearFar=[dist-cam_dh,dist+cam_dh], fov=fov_horizontal, aspect=aspect, static=None) + cam.transform.grid_size = copy.copy(resolution) + + return cam + +def build_scalarFlow_cameras(setup, ids=[2,1,0,4,3], focus_depth_clip=1.0, interpolation_weights=[]): + scalarFlow_cameras = [] + cam_resolution_scale = 1./setup.training.train_res_down #0.125#0.3 + train_cam_resolution = copy.copy(setup.rendering.main_camera.base_resolution) + train_cam_resolution[1] = int(train_cam_resolution[1]*cam_resolution_scale) + train_cam_resolution[2] = int(train_cam_resolution[2]*cam_resolution_scale) + log.info('scalarFlow train camera resolution: %s', str(train_cam_resolution)) +# cam_dh = focus_depth_clip*0.5 #depth half + + aspect = train_cam_resolution[2]/train_cam_resolution[1] + + for cam_id in ids: + cam_calib = setup.calibration[str(cam_id)] + if cam_calib.fov_horizontal is None: + cam_calib.fov_horizontal = setup.calibration.fov_horizontal_average + if setup.data.synth_shapes.active or setup.data.SDF: + cam_calib.fov_vertical = cam_calib.fov_horizontal/aspect + else: + if cam_calib.fov_vertical is None: + cam_calib.fov_vertical = setup.calibration.fov_vertical_average + + for i in range(len(ids)): + cam_calib = setup.calibration[str(ids[i])] + cam = build_camera_from_sFcallibration(**cam_calib, **setup.calibration, resolution=train_cam_resolution, focus_depth_clip=focus_depth_clip) + scalarFlow_cameras.append(cam) + + if interpolation_weights and i<(len(ids)-1): + for w in interpolation_weights: + cam_calib = interpolate_camera_callibration(setup.calibration[str(ids[i])], setup.calibration[str(ids[i+1])], w, setup.calibration) + cam = build_camera_from_sFcallibration(**cam_calib, **setup.calibration, resolution=train_cam_resolution, focus_depth_clip=focus_depth_clip) + scalarFlow_cameras.append(cam) + + return scalarFlow_cameras + +#from view_interpolation_test import get_dense_optical_flow, lerp_image, lerp_image_2, lerp_vector, slerp_vector + +def interpolate_camera_callibration(cal1, cal2, interpolation_weight, calib_base): + calib = munch.Munch() + t = interpolation_weight + calib["forward"] = slerp_vector(cal1["forward"], cal2["forward"], t, normalized=True) + calib["up"] = slerp_vector(cal1["up"], cal2["up"], t, normalized=True) + calib["right"] = slerp_vector(cal1["right"], cal2["right"], t, normalized=True) + if True: #focus_slerp is not None: + p1 = np.subtract(cal1["position"], calib_base["focus"]) + p2 = np.subtract(cal2["position"], calib_base["focus"]) + calib["position"] = np.add(slerp_vector(p1, p2, t, normalized=False), calib_base["focus"]) + else: + calib["position"] = lerp_vector(cal1["position"], cal2["position"], t) + calib["fov_horizontal"] = lerp(cal1["fov_horizontal"], cal2["fov_horizontal"], t) + calib["fov_vertical"] = lerp(cal1["fov_vertical"], cal2["fov_vertical"], t) + + return calib + +def interpolate_image(target1, target2, interpolation_weights, use_backwards_flow=True): + single = False + if np.isscalar(interpolation_weights): + single = True + interpolation_weights = [interpolation_weights] + + is_tf = False + if isinstance(target1, tf.Tensor): + target1 = target1.numpy() + is_tf = True + if isinstance(target2, tf.Tensor): + target2 = target2.numpy() + is_tf = True + + flow = get_dense_optical_flow(target1, target2) + if use_backwards_flow: + flow_back = get_dense_optical_flow(target2, target1) + targets = [lerp_image_2(target1, target2, w, flow, flow_back) if use_backwards_flow else lerp_image(target1, target2, w, flow) for w in interpolation_weights] + + if is_tf: + targets = [tf.constant(_) if len(_.shape)==3 else tf.constant(_[...,np.newaxis]) for _ in targets] + + if single: + return targets[0] + else: + return targets + +def interpolate_images(images, interpolation_weights, use_backwards_flow=True): + ret = [] + for img1, img2 in zip(images[:-1], images[1:]): + ret.append(img1) + ret.extend(interpolate_image(img1, img2, interpolation_weights, use_backwards_flow)) + ret.append(images[-1]) + return ret + +def setup_target_cameras(base_cameras, frustum_resolution, crop_coordinates=None, crop_pad=0, normalize_resolution=False, jitter=False): + cams = copy.deepcopy(base_cameras) + for cam in cams: + cam.transform.grid_size = frustum_resolution + if crop_coordinates is not None: + cams = [cam.copy_with_frustum_crop(crop_coordinates, crop_pad) for cam in cams] + #normalize cams to same grid size to allow sampling batching + if normalize_resolution: + resolutions = [cam.transform.grid_size for cam in cams] + resolution_hull = np.amax(resolutions, axis=0) + for cam in cams: + pass + if jitter: + raise NotImplementedError("TODO: fix too large uv jitter.") + for cam in cams: + cam.jitter = cam.depth_step + return cams + + +def preestimate_volume(grid_transform, targets, cameras): +#--- Volume Estimation --- + unprojections = [] + for i in range(len(cameras)): + cam = cameras[i] + #expand target to frustum volume (tile along z) + tar = tf.reshape(targets[i], [1,1] + list(targets[i].shape)) + tar = tf.tile(tar, (1,cam.transform.grid_size[0],1,1,1)) + #sample target to shared volume + unprojections.append(renderer.sample_camera(tar, grid_transform, cam, inverse=True)) + unprojection = tf.reduce_min(unprojections, axis=0) + return unprojection + +def generate_volume(grid_transform, targets, cameras, gen_model, setup, render_cameras=None, cut_alpha=True, random_rotation_pivot=None): + # set a random rotation of the (shared) volume to make the generator rotationally invariant + if random_rotation_pivot is not None: + random_rotation_pivot.rotation_deg = np.random.uniform(0,360, 3).tolist() + # get initial estimate from unprojected targets + with profiler.sample('pre-estimate volume'): + volume_estimate = preestimate_volume(sim_transform, targets, cameras) + # let the generator refine the volume + with profiler.sample('generate volume'): + volume = volume_estimate + for rec in range(setup.training.generator.recursion): + volume = tf.clip_by_value(gen_model(volume)*setup.training.generator.out_scale, setup.training.generator.out_min, setup.training.generator.out_max) + sim_transform.set_data(volume) + # render images from refined volume for loss + if render_cameras is not None: + imgs = renderer.render_density_SDF_switch(sim_transform, lights, render_cameras, cut_alpha=cut_alpha) + return volume, imgs + return volume + +def hull_AABB_OS(hull, hull_threshold = 0.1): + '''min and max coord in object-space for each axis''' + assert len(hull.get_shape().as_list())==3, "hull must be 3D DHW" + def min_max_coords(flat_hull): + coords = tf.cast(tf.where(tf.greater_equal(flat_hull, hull_threshold)), tf.float32) + min_coord = tf.minimum(tf.reduce_min(coords), tf.cast(tf.shape(flat_hull)[0], tf.float32)) + max_coord = tf.maximum(tf.reduce_max(coords), 0.) + return min_coord, max_coord + x_min, x_max = min_max_coords(tf.reduce_max(hull, axis=(-2,-3))) #W + y_min, y_max = min_max_coords(tf.reduce_max(hull, axis=(-1,-3))) #H + z_min, z_max = min_max_coords(tf.reduce_max(hull, axis=(-1,-2))) #D + return ([x_min, y_min, z_min],[x_max, y_max, z_max]) + +def create_inflow(hull, hull_height, height): + hull_threshold = 0.1 + assert len(hull.get_shape().as_list())==3, "hull must be 3D DHW" + if tf.reduce_max(hull)<1.0: + log.warning("Empty hull -> no inflow.")#raise ValueError('Empty hull') + return None, None, None + #find lowest part of hull, https://stackoverflow.com/questions/42184663/how-to-find-an-index-of-the-first-matching-element-in-tensorflow + y_hull = tf.reduce_max(hull, axis=(-1,-3)) #H + y_idx_min = tf.reduce_min(tf.where(tf.greater_equal(y_hull, hull_threshold))) + y_idx_max = y_idx_min + hull_height + + #take max xz extend of hull from hull_min to hull_min+hull_height + hull_slice = hull[:,y_idx_min:y_idx_max,:] + flat_hull_slice = tf.reduce_max(hull_slice, axis=(-2), keepdims=True) + x_hull_slice_idx = tf.where(tf.greater_equal(tf.reduce_max(flat_hull_slice, axis=(-2,-3)), hull_threshold)) + x_hull_slice_idx_min = tf.reduce_min(x_hull_slice_idx) + x_hull_slice_idx_max = tf.reduce_max(x_hull_slice_idx) +1 + z_hull_slice_idx = tf.where(tf.greater_equal(tf.reduce_max(flat_hull_slice, axis=(-2,-1)), hull_threshold)) + z_hull_slice_idx_min = tf.reduce_min(z_hull_slice_idx) + z_hull_slice_idx_max = tf.reduce_max(z_hull_slice_idx) +1 + + flat_hull_slice = flat_hull_slice[z_hull_slice_idx_min:z_hull_slice_idx_max, :, x_hull_slice_idx_min:x_hull_slice_idx_max] + + if height=='MAX': #extend inflow all the way to the lower end of the grid + height = y_idx_max.numpy().tolist() + else: + height = max(y_idx_max.numpy().tolist(), height) + inflow_mask = tf.tile(flat_hull_slice, (1,height,1)) + inflow_shape = [(z_hull_slice_idx_max-z_hull_slice_idx_min).numpy().tolist(), height, (x_hull_slice_idx_max-x_hull_slice_idx_min).numpy().tolist()] + inflow_offset = [z_hull_slice_idx_min.numpy().tolist(), (y_idx_max-height).numpy().tolist(), x_hull_slice_idx_min.numpy().tolist()] + + #return size and (corner)position + return inflow_mask, inflow_shape, inflow_offset + +# --- RENDERING --- + +def render_cameras(grid_transform, cameras, lights, renderer, img_path, name_pre='img', bkg=None, \ + format='EXR', img_transfer=None, cut_alpha=True, img_normalize=False): + + imgs = renderer.render_density_SDF_switch(grid_transform, lights, cameras, background=bkg, cut_alpha=cut_alpha) + imgs = tf.stack(imgs, axis=1) + if not cut_alpha: + imgs, imgs_d = tf.split(imgs, [3,1], axis=-1) + imgs = tf.concat([imgs, tf.exp(-imgs_d)], axis=-1) + if img_normalize: + imgs /= tf.reduce_max(imgs, axis=(-3,-2,-1), keepdims=True) + if img_transfer: + imgs = tf_element_transfer_func(imgs, img_transfer) + with renderer.profiler.sample("save image"): + renderer.write_images_batch_views(imgs, name_pre+'_b{batch:04d}_cam{view:02d}', input_format="NVHWC", base_path=img_path, image_format=format) + +def render_cycle(grid_transform, cameras, lights, renderer, img_path, name_pre='img', steps=12, steps_per_cycle=12, bkg=None, \ + format='EXR', img_transfer=None, img_stats=True, rotate_cameras=False, cut_alpha=True, img_normalize=False): + + r_step = 360.0/steps_per_cycle + + if renderer.can_render_fused and rotate_cameras: + cams = [] + for camera in cameras: + for i in range(steps): + cam = copy.deepcopy(camera) + cam.transform.parent.add_rotation_deg(y=-r_step*i) + cams.append(cam) + if bkg is not None: + bkg = [_ for _ in bkg for i in range(steps)] + render_cameras(grid_transform, cams, lights, renderer, img_path, name_pre, bkg, format, img_transfer, cut_alpha, img_normalize) + return + + if rotate_cameras: + cameras = copy.deepcopy(cameras) + else: + rot = grid_transform.rotation_deg + grid_transform.rotation_deg = [0,0,0] + with renderer.profiler.sample("render cycle "+name_pre): + for i in range(steps): + if not rotate_cameras: + grid_transform.rotation_deg = [0,i*r_step,0] + with renderer.profiler.sample("render step"): + imgs = renderer.render_density_SDF_switch(grid_transform, lights, cameras, background=bkg, cut_alpha=cut_alpha)#, background=bkg + imgs = tf.concat(imgs, axis=0) + if not cut_alpha: + imgs, imgs_d = tf.split(imgs, [3,1], axis=-1) + imgs = tf.concat([imgs, tf.exp(-imgs_d)], axis=-1) + if img_normalize: + imgs /= tf.reduce_max(imgs, axis=(-3,-2,-1), keepdims=True) + if img_transfer: + if isinstance(img_transfer, tuple): + imgs = tf_cmap_nearest(imgs, *img_transfer) + else: + imgs = tf_element_transfer_func(imgs, img_transfer) + if args.console and img_stats: + print_stats(imgs, 'frame '+str(i)) + with renderer.profiler.sample("save image"): + renderer.write_images([imgs], [name_pre+'_cam{}_{:04d}'], base_path=img_path, use_batch_id=True, frame_id=i, format=format) + if rotate_cameras: + for camera in cameras: + camera.transform.parent.add_rotation_deg(y=-r_step)#counter-rotate cam to match same object-view as object rotation + if not rotate_cameras: grid_transform.rotation_deg = rot + +def _slice_single_channel_color_transfer(data): + assert isinstance(data, tf.Tensor) + data_shape = shape_list(data) + assert len(data_shape)>=1 + assert data_shape[-1]==1 + + #return tf.concat([tf.maximum(data,0), tf.abs(data), tf.maximum(-data, 0)], axis=-1) + # with narrow band + return tf.concat([tf.maximum(data,0), tf.abs(data), tf.maximum(-data, 0), tf.cast(tf.less_equal(tf.abs(data), 1.6), tf.float32)], axis=-1) + +def render_slices(data, slices, img_path, name_pre='slc', format='EXR', normalize=False, slice_indices=None): + + assert isinstance(slices, list) + assert isinstance(data, tf.Tensor) + assert len(shape_list(data))==5 + + data_shape = GridShape.from_tensor(data) + + if normalize: + data = data * (1.0/tf.reduce_max(tf.abs(data), axis=(-4,-3,-2,-1), keepdims=True)) + + if data_shape.c==1: + data = _slice_single_channel_color_transfer(data) + + if "X" in slices: + data_slices = tf.unstack(data, axis=-2) #V-NHWC + if slice_indices is not None: + data_slices = [data_slices[_] for _ in slice_indices] + renderer.write_images_batch_views(data_slices, name_pre + '_b{batch:04d}_camX{view:02d}', base_path=img_path, frame_idx=None, image_format=format) + + if "Y" in slices: + data_slices = tf.unstack(data, axis=-3) #V-NHWC + if slice_indices is not None: + data_slices = [data_slices[_] for _ in slice_indices] + renderer.write_images_batch_views(data_slices, name_pre + '_b{batch:04d}_camY{view:02d}', base_path=img_path, frame_idx=None, image_format=format) + + if "Z" in slices: + data_slices = tf.unstack(data, axis=-4) #V-NHWC + if slice_indices is not None: + data_slices = [data_slices[_] for _ in slice_indices] + renderer.write_images_batch_views(data_slices, name_pre + '_b{batch:04d}_camZ{view:02d}', base_path=img_path, frame_idx=None, image_format=format) + +def render_gradients(gradients, grid_transform, cameras, renderer, path, image_mask, steps=12, steps_per_cycle=12, format='EXR', img_stats=True, name="gradients", log=None): + tf_print_stats(gradients, "gradients " + name, log=log) + os.makedirs(path, exist_ok=True) + grad_shape = GridShape.from_tensor(gradients) + if grad_shape.c==1: #density gradients + grad_light = tf.concat([tf.maximum(gradients,0), tf.zeros_like(gradients), tf.maximum(-gradients, 0)], axis=-1) + elif grad_shape.c==3: #velocity gradients + grad_light = tf.abs(gradients) + grid_transform = grid_transform.copy_new_data(tf.zeros_like(gradients)) + grid_transform.rotation_deg = [0,0,0] + r_step = 360.0/steps_per_cycle + with renderer.profiler.sample("render gradients cycle"): + for i in range(steps): + grid_transform.rotation_deg = [0,i*r_step,0] + with renderer.profiler.sample("render step"): + imgs = renderer.render_density(grid_transform, [grad_light], cameras, cut_alpha=True) + imgs = tf.stack(imgs, axis=0) #VNHWC + imgs /=tf.reduce_max(imgs) + with renderer.profiler.sample("save image"): + renderer.write_images_batch_views(imgs, image_mask, input_format="VNHWC", base_path=path, frame_idx=i, image_format=format) + +def write_image_gradients(gradient_images, renderer, path, image_mask, image_neg_mask, format='EXR', img_stats=True): + os.makedirs(path, exist_ok=True) + if args.console and img_stats: + print_stats(gradient_images, 'gradients frame '+str(i)) + imgs = gradient_images / tf.reduce_max(tf.abs(gradient_images)) + imgs_neg = tf.maximum(-imgs, 0) + imgs = tf.maximum(imgs, 0) + with renderer.profiler.sample("save image"): + renderer.write_images([imgs, imgs_neg], [image_mask, image_neg_mask], base_path=path, use_batch_id=True, format=format) + +''' +def advect_step(density, velocity): + density = velocity.warp(density) + velocity = velocity.copy_warped() + return density, velocity +''' +def world_scale(shape, size=None, width=None, as_np=True): + ''' + shape and size are z,y,x + width corresponds to x and keeps aspect/cubic cells + ''' + assert len(shape)==3 + if size is not None and width is not None: + raise ValueError("Specify only one of size or width.") + if size is not None: + assert len(size)==3 + scale = np.asarray(size, dtype=np.float32)/np.asarray(shape, dtype=np.float32) + elif width is not None: + scale = np.asarray([width/shape[-1]]*3, dtype=np.float32) + else: + raise ValueError("Specify one of size or width.") + if as_np: + return scale + else: + return scale.tolist() + +SDC = 256 #step density correction +# loss weight corrections after mean/sum reduction changes +tar_c = 1/(320 * 180) +grid_c = 1/(128*227*128) +train_dens = False + +del SDC +if __name__=='__main__': + from common_setups import RECONSTRUCT_SEQUENCE_SETUP_NEURAL_DENSITY + if args.setup_file is not None: + try: + with open(args.setup_file, 'r') as setup_json: + setup = json.load(setup_json) + except: + raise + else: + raise RuntimeError("No setup specified.") + setup = update_dict_recursive(RECONSTRUCT_SEQUENCE_SETUP_NEURAL_DENSITY, setup, deepcopy=True, new_key='DISCARD_WARN') # new_key: DISCARD_WARN, ERROR + + with open(setup["rendering"]["target_cameras"]["calibration_file"], 'r') as calibration_file: + cam_setup = json.load(calibration_file) + setup['calibration']=cam_setup + def flip_z(v): + return v*np.asarray([1,1,-1]) + + setup = munch.munchify(setup) + cam_setup = setup.calibration + + hostname = socket.gethostname() + now = datetime.datetime.now() + now_str = now.strftime("%y%m%d-%H%M%S") + try: + paths = setup.paths + except AttributeError: + setup.paths = munch.Munch() + prefix = 'seq' + try: + base_path = setup.paths.base + except AttributeError: + setup.paths.base = "./" + base_path = setup.paths.base + try: + run_path = setup.paths.run + except AttributeError: + if args.fit: + setup.paths.run = 'recon_{}_{}_{}'.format(prefix, now_str, setup.title) + else: + setup.paths.run = 'render_{}_{}_{}'.format(prefix, now_str, setup.title) + if hasattr(setup.paths, 'group'): + setup.paths.path = os.path.join(setup.paths.base, setup.paths.group, setup.paths.run) + else: + setup.paths.path = os.path.join(setup.paths.base, setup.paths.run) + + + if os.path.isdir(setup.paths.path): + setup.paths.path, _ = makeNextGenericPath(setup.paths.path) + else: + os.makedirs(setup.paths.path) + + setup.paths.log = os.path.join(setup.paths.path, 'log') + os.makedirs(setup.paths.log) + setup.paths.config = os.path.join(setup.paths.path, 'config') + os.makedirs(setup.paths.config) + setup.paths.data = setup.paths.path + if setup.validation.warp_test: + setup.paths.warp_test = os.path.join(setup.paths.path, 'warp_test') + os.makedirs(setup.paths.warp_test) + + sys.stderr = StreamCapture(os.path.join(setup.paths.log, 'stderr.log'), sys.stderr) + + #setup logging + log_format = '[%(asctime)s][%(name)s:%(levelname)s] %(message)s' + log_formatter = logging.Formatter(log_format) + root_logger = logging.getLogger() + root_logger.setLevel(logging.INFO) + logfile = logging.FileHandler(os.path.join(setup.paths.log, 'logfile.log')) + logfile.setLevel(logging.INFO) + logfile.setFormatter(log_formatter) + root_logger.addHandler(logfile) + errlog = logging.FileHandler(os.path.join(setup.paths.log, 'error.log')) + errlog.setLevel(logging.WARNING) + errlog.setFormatter(log_formatter) + root_logger.addHandler(errlog) + if args.debug: + debuglog = logging.FileHandler(os.path.join(setup.paths.log, 'debug.log')) + debuglog.setLevel(logging.DEBUG) + debuglog.setFormatter(log_formatter) + root_logger.addHandler(debuglog) + if args.console: + console = logging.StreamHandler(sys.stdout) + console.setLevel(logging.INFO) + console_format = logging.Formatter('[%(name)s:%(levelname)s] %(message)s') + console.setFormatter(console_format) + root_logger.addHandler(console) + log = logging.getLogger('train') + log.setLevel(logging.DEBUG) + + logging.captureWarnings(True) + + if args.debug: + root_logger.setLevel(logging.DEBUG) + log.info("Debug output active") + + + + with open(os.path.join(setup.paths.config, 'setup.json'), 'w') as config: + json.dump(setup, config, sort_keys=True, indent=2) + + sources = [sys.argv[0], "common_setups.py"] + sources.extend(os.path.join("lib", _) for _ in os.listdir("lib") if _.endswith(".py")) + sources.extend(os.path.join("phitest/render", _) for _ in os.listdir("phitest/render") if _.endswith(".py")) + archive_files(os.path.join(setup.paths.config,'sources.zip'), *sources) + + log.info('--- Running test: %s ---', setup.title) + log.info('Test description: %s', setup.desc) + log.info('Test directory: %s', setup.paths.path) + log.info('Python: %s', sys.version) + log.info('TensorFlow version: %s', tf.__version__) + log.info('host: %s, device: %s, pid: %d', hostname, cudaID, os.getpid()) + + + max_memory = args.max_memory + if max_memory<=0: + log.info("No memory limit.") + max_memory = -1 + if max_memory<1: + max_memory = int(psutil.virtual_memory().total * max_memory) + else: + max_memory = int(min(max_memory * 1024 * 1024, psutil.virtual_memory().total)) + if max_memory>0: + log.info("Memory limit: %d MiB (%f%%).", max_memory/(1024*1024), max_memory/psutil.virtual_memory().total * 100) + + if setup.data.rand_seed_global is not None: + os.environ['PYTHONHASHSEED']=str(setup.data.rand_seed_global) + random.seed(setup.data.rand_seed_global) + np.random.seed(setup.data.rand_seed_global) + tf.set_random_seed(setup.data.rand_seed_global) + log.info("global random seed: %s", setup.data.rand_seed_global) + + profiler = DEFAULT_PROFILER #Profiler() + renderer = Renderer(profiler, + filter_mode=setup.rendering.filter_mode, + boundary_mode= setup.rendering.boundary if setup.rendering.boundary is not None else ("CLAMP" if setup.data.SDF else "BORDER"), + mipmapping=setup.rendering.mip.mode, + num_mips=setup.rendering.mip.level, + mip_bias=setup.rendering.mip.bias, + blend_mode=setup.rendering.blend_mode, + SDF_threshold=setup.rendering.SDF_threshold, + sample_gradients=setup.rendering.sample_gradients, + fast_gradient_mip_bias_add=0.0, + luma = setup.rendering.luma, + fused=setup.rendering.allow_fused_rendering, + render_as_SDF=setup.data.SDF) + vel_renderer = Renderer(profiler, + filter_mode=setup.rendering.filter_mode, + mipmapping=setup.rendering.mip.mode, + num_mips=setup.rendering.mip.level, + mip_bias=setup.rendering.mip.bias, + blend_mode='ADDITIVE', + SDF_threshold=setup.rendering.SDF_threshold, + sample_gradients=setup.rendering.sample_gradients, + luma = setup.rendering.luma, + fused=setup.rendering.allow_fused_rendering) + scale_renderer = Renderer(profiler, + filter_mode='LINEAR', + boundary_mode=setup.data.velocity.boundary.upper(), + mipmapping=setup.rendering.mip.mode, + num_mips=setup.rendering.mip.level, + mip_bias=setup.rendering.mip.bias, + blend_mode='ADDITIVE', + SDF_threshold=setup.rendering.SDF_threshold, + sample_gradients=setup.rendering.sample_gradients, + render_as_SDF=setup.data.SDF) + density_sampler = Renderer(profiler, + filter_mode=setup.rendering.filter_mode, + boundary_mode="CLAMP" if setup.data.SDF else "BORDER", + mipmapping=setup.rendering.mip.mode, + num_mips=setup.rendering.mip.level, + mip_bias=setup.rendering.mip.bias, + blend_mode='ADDITIVE', + SDF_threshold=setup.rendering.SDF_threshold, + sample_gradients=setup.rendering.sample_gradients, + render_as_SDF=setup.data.SDF) + warp_renderer = Renderer(profiler, + filter_mode='LINEAR', + boundary_mode=setup.data.velocity.boundary.upper(), + mipmapping='NONE', + blend_mode='ADDITIVE', + SDF_threshold=setup.rendering.SDF_threshold, + sample_gradients=False, + render_as_SDF=setup.data.SDF) + + synth_target_renderer = Renderer(profiler, + filter_mode=setup.rendering.synthetic_target.filter_mode, + boundary_mode= setup.rendering.boundary if setup.rendering.boundary is not None else ("CLAMP" if setup.data.SDF else "BORDER"), + mipmapping=setup.rendering.mip.mode, + num_mips=setup.rendering.mip.level, + mip_bias=setup.rendering.mip.bias, + blend_mode=setup.rendering.synthetic_target.blend_mode, + SDF_threshold=setup.rendering.SDF_threshold if setup.data.synth_shapes.active else 0.5, # 0.5 for ShapeNet SDF + sample_gradients=setup.rendering.sample_gradients, + fast_gradient_mip_bias_add=0.0, + luma = setup.rendering.luma, + fused=setup.rendering.allow_fused_rendering, + render_as_SDF=setup.data.SDF) + + upscale_renderer = Renderer(profiler, + filter_mode='LINEAR', + boundary_mode=setup.data.velocity.boundary.upper(), + mipmapping='NONE', + blend_mode='ADDITIVE', + SDF_threshold=setup.rendering.SDF_threshold, + sample_gradients=setup.rendering.sample_gradients, + luma = setup.rendering.luma) + lifting_renderer = Renderer(profiler, + filter_mode='LINEAR', + boundary_mode="BORDER", + mipmapping='NONE', + blend_mode='ADDITIVE', + SDF_threshold=setup.rendering.SDF_threshold, + sample_gradients=setup.rendering.sample_gradients, + luma = setup.rendering.luma) + max_renderer = Renderer(profiler, + filter_mode=setup.rendering.filter_mode, + mipmapping=setup.rendering.mip.mode, + num_mips=setup.rendering.mip.level, + mip_bias=setup.rendering.mip.bias, + blend_mode='MAX', + SDF_threshold=setup.rendering.SDF_threshold, + sample_gradients=setup.rendering.sample_gradients, + fast_gradient_mip_bias_add=0.0, + fused=setup.rendering.allow_fused_rendering) + + grad_renderer = vel_renderer + + pFmt = PartialFormatter() + run_index = RunIndex(setup.data.run_dirs, ['recon_seq',]) + + + def load_model(path, *, num_levels, input_merge_weight=None, skip_merge_weight=None, output_residual_weight=None, **load_kwargs): + model_path = run_index[path] + if model_path is None: + model_path = path + config_path = model_path + ".json" + if os.path.isfile(config_path): + with open(config_path, "r") as config_file: + model_config = json.load(config_file) + model_config = munch.munchify(model_config) + + if model_config._config.name=="RWDensityGeneratorNetwork": + single_view = True + return RWDensityGeneratorNetwork.load(config_path, input_channels=1, w1=(1.0 if single_view else 0.5), w2=(0 if single_view else 0.5)) + + if model_config._config.name=="RWVelocityGeneratorNetwork": + return RWVelocityGeneratorNetwork.load(config_path, dens_channels=1, unp_channels=1, use_proxy=True) + + variable_level_model = GrowingUNet.config_is_level_variable(model_path + ".json") + if variable_level_model: + max_levels = num_levels + log.info("Loading variable level model with %d levels.", max_levels) + model = GrowingUNet.load(model_path+".json", num_levels=max_levels, **load_kwargs) + model.set_active_level(max_levels-1) + else: + model = GrowingUNet.load(model_path+".json", **load_kwargs) + log.info("Loaded fixed level model with %d levels for resolution %s", model.num_levels, sim_transform.grid_size) + + max_levels = model.num_levels + + if model.down_mode != "NONE" and input_merge_weight is not None: + log.info("Setting input merge weights to %f.", input_merge_weight) + for l in range(max(0, max_levels - model.max_input_levels), max_levels-1): + model.set_input_merge_weight(input_merge_weight,l) + if model.skip_merge_mode != "SUM" and skip_merge_weight is not None: + log.info("Setting skip merge weights to %f.", skip_merge_weight) + for l in range(1, max_levels): + model.set_skip_merge_weight(skip_merge_weight,l) + if model.output_mode=="RESIDUAL_WEIGHTED" and output_residual_weight is not None: + log.info("Setting output residual weights to %f.", output_residual_weight) + for l in range(1, max_levels): + model.set_output_residual_weight(output_residual_weight, l) + + elif os.path.isfile(model_path + "_model.h5"): + log.warning("No UNet spec found, loading plain keras model.") + model = tf.keras.models.load_model(model_path + "_model.h5", custom_objects=custom_keras_objects) + else: + raise IOError("Can't load UNet or keras model from '%s'"%(model_path,)) + if False: #copy weights from broken serialization + log.warning("Replacing model weights") + model.load_weights(run_index[setup.training.density.decoder.model.replace("init", "model_nonorm")], by_name=True) + save_NNmodel(model, 'density_decoder', setup.paths.data) + + return model + + def load_velocity(mask, fmt=None, boundary=None, scale_renderer=None, warp_renderer=None, device=None, var_name="velocity"): + sf = RunIndex.parse_scalarFlow(mask) + load_mask = run_index[mask] + if load_mask is not None: + load_mask = pFmt.format(load_mask, **fmt) if fmt is not None else load_mask + log.info("load velocity grid from run %s", load_mask) + vel_grid = VelocityGrid.from_file(load_mask, boundary=boundary, scale_renderer=scale_renderer, warp_renderer=warp_renderer, device=device, var_name=var_name) + elif sf is not None: #mask.startswith('[SF:') and id_end!=-1: # explicit scalarFlow + if sf["frame"] is None: + raise ValueError("Missing frame from scalarFlow specifier.") + fmt['sim'] += sf["sim"] + fmt['frame'] += sf["frame"] + run_path = os.path.normpath(os.path.join(setup.data.velocity.scalarFlow_reconstruction, sf["relpath"])).format(**fmt) + log.info("load velocity grid from ScalarFlow %s", run_path) + vel_grid = VelocityGrid.from_scalarFlow_file(run_path, boundary=boundary, scale_renderer=scale_renderer, warp_renderer=warp_renderer, device=device, var_name=var_name) + else: # not in runs, assume centered vel + load_mask = mask.format(**fmt) if fmt is not None else mask + log.info("load centered velocity grid from file %s", load_mask) + with np.load(load_mask) as np_data: + vel_centered = reshape_array_format(np_data['data'], 'DHWC') + vel_grid = VelocityGrid.from_centered(tf.constant(vel_centered, dtype=tf.float32), boundary=boundary, scale_renderer=scale_renderer, warp_renderer=warp_renderer, device=device, var_name=var_name) + return vel_grid + + custom_keras_objects = { + 'MirrorPadND':MirrorPadND, + 'LayerNormalization':LayerNormalization, + 'AdaptiveNormalization':AdaptiveNormalization, + 'ConvLayerND':ConvLayerND, + 'ResBlock':ResBlock, + 'DenseConvBlock':DenseConvBlock, + 'WeightedSum':WeightedSum, + 'ScalarMul':ScalarMul, + } + + color_channel = 1 if setup.rendering.monochrome else 3 + + # automatic target scaling to grid resolution + if True: + def get_res_down(base_grid_size, base_factor=128 * 6): + return int(base_factor / base_grid_size) + res_down = get_res_down(setup.data.grid_size, base_factor=setup.data.res_down_factor) + log.info("Setting automatic target/image resolution down-scale to %d for grid resolution %d", res_down, setup.data.grid_size) + setup.training.train_res_down = res_down + setup.training.discriminator.cam_res_down = res_down + setup.data.discriminator.real_res_down = res_down + + + train_res_down=setup.training.train_res_down + + density_size = [setup.data.grid_size]*3 + if setup.data.y_scale=="SF": + setup.data.y_scale = cam_setup.scale_y + density_size[1] = int(math.ceil(density_size[1] * setup.data.y_scale)) + volume_size = [cam_setup.marker_width, cam_setup.marker_width * cam_setup.scale_y, cam_setup.marker_width] + sim_center = flip_z(cam_setup.volume_offset + np.asarray(volume_size)/2.0) + def make_sim_transform(): + calibration_transform = Transform(translation=sim_center, scale=[cam_setup.marker_width]*3) + randomization_transform = Transform(parent=calibration_transform) + normalization_transform = GridTransform(density_size, center=True, normalize='MIN', parent=randomization_transform) + return normalization_transform + sim_transform = make_sim_transform() + log.info("Base sim transform: %s", sim_transform) + if True: + sim_transform.parent.parent.translation[1]=sim_transform.grid_size_world().y/2 + (0.05 if setup.data.density.render_targets else 0.005) + log.info("Set domain Y-center to %f for SF data inflow handling", sim_transform.parent.parent.translation[1]) + sF_transform = GridTransform([100,178,100], translation=flip_z(cam_setup.volume_offset + np.asarray([0,0,cam_setup.marker_width])), scale=[cam_setup.marker_width]*3, normalize='MIN') + density_size = Int3(density_size[::-1]) + + cam_resolution = copy.copy(setup.rendering.main_camera.base_resolution) + aspect = cam_resolution[2]/cam_resolution[1] + cam_resolution[1] *= setup.rendering.main_camera.resolution_scale + cam_resolution[2] *= setup.rendering.main_camera.resolution_scale + cam_dist = setup.rendering.main_camera.distance + main_camera = Camera(GridTransform(cam_resolution, translation=[0,0,cam_dist], parent=Transform(translation=[0.33913451, 0.38741691, -0.25786148], rotation_deg=[0,0,0.])), nearFar=[setup.rendering.main_camera.near,setup.rendering.main_camera.far], + fov = setup.rendering.main_camera.fov, aspect=aspect) + cameras = [ + main_camera, + ] + if not args.fit: + tmp_cam = copy.deepcopy(main_camera) + tmp_cam.transform.parent.rotation_deg = [0,90,0] + cameras.append(tmp_cam) + tmp_cam = copy.deepcopy(main_camera) + tmp_cam.transform.parent.rotation_deg = [0,225,0] + cameras.append(tmp_cam) + del tmp_cam + else: + tmp_cam = copy.deepcopy(main_camera) + tmp_cam.transform.parent.rotation_deg = [0,90,0] + cameras.append(tmp_cam) + del tmp_cam + + for _cam in cameras: + renderer.check_LoD(sim_transform, _cam, check_inverse=True, name="main camera") + + scalarFlow_cam_ids = setup.rendering.target_cameras.camera_ids #[2,1,0,4,3] #[0,2,3] # + + if setup.data.density.target_cam_ids =="ALL": + setup.data.density.target_cam_ids = list(range(len(scalarFlow_cam_ids))) + + view_interpolation_weights = [(_+1)/(setup.training.density.view_interpolation.steps+1) for _ in range(setup.training.density.view_interpolation.steps)] + scalarFlow_cameras = build_scalarFlow_cameras(setup, scalarFlow_cam_ids, interpolation_weights=view_interpolation_weights) + scalarFlow_cameras_base = [scalarFlow_cameras[_*(setup.training.density.view_interpolation.steps+1)] for _ in range(len(scalarFlow_cam_ids))] + + scalarFlow_cam_focus = flip_z(setup.calibration.focus) + cam_resolution_scale = 1./setup.training.train_res_down + train_cam_resolution = copy.copy(setup.rendering.main_camera.base_resolution) + train_cam_resolution[1] = int(train_cam_resolution[1]*cam_resolution_scale) + train_cam_resolution[2] = int(train_cam_resolution[2]*cam_resolution_scale) + log.info('scalarFlow train camera resolution: %s', str(train_cam_resolution)) + for sF_cam in scalarFlow_cameras: + renderer.check_LoD(sim_transform, sF_cam, check_inverse=True, name="scalarFlow camera") + cam_dh = 0.5 #depth half + if setup.training.discriminator.active: + disc_cam_resolution_scale = 1./setup.training.discriminator.cam_res_down + disc_cam_resolution = copy.copy(setup.rendering.main_camera.base_resolution) + disc_cam_resolution[1] = int(disc_cam_resolution[1]*disc_cam_resolution_scale) + disc_cam_resolution[2] = int(disc_cam_resolution[2]*disc_cam_resolution_scale) + disc_cam_dist = 1.3 + disc_camera = Camera(GridTransform(disc_cam_resolution, translation=[0,0,disc_cam_dist], parent=Transform(translation=scalarFlow_cam_focus, rotation_deg=[0,0,0])), nearFar=[disc_cam_dist-cam_dh,disc_cam_dist+cam_dh], fov=cam_setup.fov_horizontal_average, aspect=aspect) + disc_cameras = [copy.deepcopy(disc_camera) for _ in range(setup.training.discriminator.num_fake)] + if setup.training.discriminator.fake_camera_jitter: + raise NotImplementedError("TODO: fix too large uv jitter.") + for cam in disc_cameras: + cam.jitter = cam.depth_step + log.info('discriminator camera resolution: %s, jitter: %s', str(disc_cam_resolution), setup.training.discriminator.fake_camera_jitter) + renderer.check_LoD(sim_transform, disc_camera, check_inverse=True, name="discriminator camera") + log.debug('Main camera transform: %s', main_camera.transform) + + lights = [] + + if setup.rendering.lighting.initial_intensity=="CAMLIGHT": + log.info("Using camlight lighting for rendering.") + if not setup.data.SDF: + raise ValueError("Camlight requires SDF mode.") + lights = "CAMLIGHT" + else: + if setup.rendering.lighting.initial_intensity>=0: + if setup.data.SDF: + lights.append( # left red light + PointLight(Transform(translation=[0,0,4], parent= \ + Transform(rotation_deg=[0,0,0], parent= \ + Transform(translation=scalarFlow_cam_focus, rotation_deg=[0,-120,0]))), \ + range_scale=0.5, + intensity=setup.rendering.lighting.initial_intensity, \ + color=[1,0,0], \ + ) + ) + lights.append( # center top green light + PointLight(Transform(translation=[0,0,4], parent= \ + Transform(rotation_deg=[0,0,0], parent= \ + Transform(translation=scalarFlow_cam_focus, rotation_deg=[0,0,0]))), \ + range_scale=0.5, + intensity=setup.rendering.lighting.initial_intensity, \ + color=[0,1,0], \ + ) + ) + lights.append( # right blue light + PointLight(Transform(translation=[0,0,4], parent= \ + Transform(rotation_deg=[0,0,0], parent= \ + Transform(translation=scalarFlow_cam_focus, rotation_deg=[0,120,0]))), \ + range_scale=0.5, + intensity=setup.rendering.lighting.initial_intensity, \ + color=[0,0,1], \ + ) + ) + + else: + lights.append( + SpotLight(Transform(translation=[0,0,2], parent=Transform(translation=scalarFlow_cam_focus, rotation_deg=[-40,0,0])), intensity=setup.rendering.lighting.initial_intensity, \ + cast_shadows=True, shadow_clip=[1.35, 2.65], range_scale=0.825, angle_deg=25., shadow_resolution=setup.rendering.lighting.shadow_resolution, cone_mask=False, \ + static=sim_transform if setup.rendering.allow_static_cameras else None) + ) + + if setup.rendering.lighting.ambient_intensity>=0: + lights.append(Light(intensity=setup.rendering.lighting.ambient_intensity)) #some simple constant/ambient light as scattering approximation + + for light in lights: + if isinstance(light, SpotLight) and light.cast_shadows: + renderer.check_LoD(sim_transform, light.shadow_cam, check_inverse=True, name="shadow camera") + + + shadow_lights = [ + SpotLight(Transform(translation=[0,0,2], parent=Transform(translation=scalarFlow_cam_focus, rotation_deg=[-40,0,0])), intensity=2.0, \ + cast_shadows=True, shadow_clip=[1.35, 2.65], range_scale=0.825, angle_deg=25., shadow_resolution=setup.rendering.lighting.shadow_resolution, cone_mask=False, \ + static=sim_transform if setup.rendering.allow_static_cameras else None), + Light(intensity=0.08), + ] + + synth_target_lights = [] + if setup.rendering.synthetic_target.initial_intensity>=0: + synth_target_lights.append( + SpotLight(Transform(translation=[0,0,2], parent=Transform(translation=scalarFlow_cam_focus, rotation_deg=[-40,0,0])), intensity=setup.rendering.synthetic_target.initial_intensity, \ + cast_shadows=True, shadow_clip=[1.35, 2.65], range_scale=0.825, angle_deg=25., shadow_resolution=setup.rendering.lighting.shadow_resolution, cone_mask=False, \ + static=sim_transform if setup.rendering.allow_static_cameras else None) + ) + + if setup.rendering.synthetic_target.ambient_intensity>=0: + synth_target_lights.append(Light(intensity=setup.rendering.synthetic_target.ambient_intensity)) + + + if not args.fit: + # scene serialization + scene = { + "cameras":cameras, + "sFcameras":scalarFlow_cameras, + "lighting":lights, + "objects":[sim_transform], + } + scene_file = os.path.join(setup.paths.config, "scene.json") + #log.debug("Serializing scene to %s ...", scene_file) + with open(scene_file, "w") as file: + try: + json.dump(scene, file, default=to_dict, sort_keys=True)#, indent=2) + except: + log.exception("Scene serialization failed.") + + main_render_ctx = RenderingContext([main_camera], lights, renderer, vel_renderer, setup.rendering.monochrome, render_SDF=setup.data.SDF) + + def get_validation_sequence_step(setup): + return max(setup.data.sequence_step) if isinstance(setup.data.sequence_step, collections.abc.Iterable) else setup.data.sequence_step + def get_frame_step(setup): + return get_validation_sequence_step(setup) if (setup.data.randomize>0 and args.fit) else setup.data.step + def get_vel_render_scale(setup): + return 1.0 / float(get_frame_step(setup))*setup.rendering.velocity_scale + def get_vel_scale_for_render(setup, transform): + return transform.cell_size_world().value * get_vel_render_scale(setup) + + def render_sequence(sequence, vel_pad, cycle=True, cycle_steps=12, sF_cam=False, render_density=True, render_shadow=True, render_velocity=True, render_MS=True, slices=None): + log.debug("Render images for sequence") + clip_cams = False #True + with profiler.sample('render sequence'): + if cycle: + cycle_cams = [main_camera] + + if render_shadow: + shadow_cams = [copy.deepcopy(main_camera) for _ in range(1)] + shadow_cams[0].transform.parent.add_rotation_deg(y=-60) + shadow_cams_cycle = [main_camera] + shadow_dens_scale = 4. + + if clip_cams: + AABB_corners_WS = [] + AABB_corners_WS_cycle = [] + GRID_corners_WS_cycle = [] + for state in sequence: + dens_transform = state.get_density_transform() + dens_hull = state.density.hull + if dens_hull is None: + continue + corners_OS = hull_AABB_OS(tf.squeeze(dens_hull, (0,-1))) + AABB_corners_WS += dens_transform.transform_AABB(*corners_OS, True) + + dens_shape = dens_transform.grid_shape + grid_OS = (np.asarray([0,0,0], dtype=np.float32), np.asarray(dens_shape.xyz, dtype=np.float32)) + cycle_transform = dens_transform.copy_no_data() + AABB_corners_WS_cycle.extend(cycle_transform.transform_AABB(*corners_OS, True)) + GRID_corners_WS_cycle.extend(cycle_transform.transform_AABB(*grid_OS, True)) + for i in range(1, cycle_steps): + cycle_transform.add_rotation_deg(y=i * 360/cycle_steps) #rotation_deg[1] += i * 360/cycle_steps + AABB_corners_WS_cycle.extend(cycle_transform.transform_AABB(*corners_OS, True)) + GRID_corners_WS_cycle.extend(cycle_transform.transform_AABB(*grid_OS, True)) + + del dens_hull + if AABB_corners_WS: + seq_cams = [cam.copy_clipped_to_world_coords(AABB_corners_WS)[0] for cam in cameras] + else: + seq_cams = cameras + + if cycle and AABB_corners_WS_cycle: + cycle_cams = [cam.copy_clipped_to_world_coords(AABB_corners_WS_cycle)[0] for cam in cycle_cams] + + if render_shadow and GRID_corners_WS_cycle: + shadow_cams = [cam.copy_clipped_to_world_coords(GRID_corners_WS_cycle)[0] for cam in shadow_cams] + if cycle: + shadow_cams_cycle = [cam.copy_clipped_to_world_coords(GRID_corners_WS_cycle)[0] for cam in shadow_cams_cycle] + + split_cams = True + else: + seq_cams = cameras + split_cams = False + + i=0 + if args.console: + substeps = 0 + if render_density: + substeps += 3 if cycle else 1 + if sF_cam: substeps += 1 + if slices is not None: substeps += 1 + if render_velocity: substeps += 3 if cycle else 1 + cycle_pbar = ProgressBar(len(sequence)*substeps, name="Render Sequence: ") + substep = 0 + def update_pbar(frame, desc): + nonlocal substep + cycle_pbar.update(i*substeps + substep, desc="Frame {:03d} ({:03d}/{:03d}): {:30}".format(frame, i+1, len(sequence), desc)) + substep +=1 + + for state in sequence: + if render_density: + log.debug("Render density frame %d (%d)", state.frame, i) + if args.console: update_pbar(state.frame, "Density, main cameras") + bkg_render = None + bkg_render_alpha = None + if setup.rendering.background.type=='CAM': + bkg_render = state.bkgs + elif setup.rendering.background.type=='COLOR': + bkg_render = [tf.constant(setup.rendering.background.color, dtype=tf.float32)]*len(seq_cams) + bkg_render_alpha = [tf.constant(list(setup.rendering.background.color) + [0.], dtype=tf.float32)]*len(seq_cams) + dens_transform = state.get_density_transform() + val_imgs = renderer.render_density_SDF_switch(dens_transform, lights, seq_cams, background=bkg_render, split_cameras=split_cams) + renderer.write_images_batch_views(val_imgs, 'seq_img_b{batch:04d}_cam{view:02d}_{idx:04d}', base_path=setup.paths.data, frame_idx=i, image_format='PNG') + if state.has_density_proxy: + dens_proxy_transform = dens_transform.copy_new_data(state.density_proxy.d) + val_imgs = renderer.render_density_SDF_switch(dens_proxy_transform, lights, seq_cams, background=bkg_render, split_cameras=split_cams) + renderer.write_images_batch_views(val_imgs, 'seq_imgP_b{batch:04d}_cam{view:02d}_{idx:04d}', base_path=setup.paths.data, frame_idx=i, image_format='PNG') + if render_shadow: + shadow_dens = dens_transform.copy_new_data(render_helper.with_border_planes(dens_transform.data *shadow_dens_scale, planes=["Z-","Y-"], density=100., width=3, offset=2)) + shadow_imgs = renderer.render_density_SDF_switch(shadow_dens, shadow_lights, shadow_cams, background=bkg_render, split_cameras=split_cams) + renderer.write_images_batch_views(shadow_imgs, 'seq_sdw_b{batch:04d}_cam{view:02d}_{idx:04d}', base_path=setup.paths.data, frame_idx=i, image_format='PNG') + if cycle or sF_cam: + tmp_transform = state.get_density_transform() + tmp_transform.set_data(tf.zeros_like(state.density.d)) + dens_grads = ("viridis", 0., 2.5) + if cycle: + if args.console: update_pbar(state.frame, "Density, cycle") + render_cycle(dens_transform, cycle_cams, lights, renderer, state.data_path, steps=cycle_steps, steps_per_cycle=cycle_steps, name_pre='img', bkg=bkg_render_alpha, img_stats=False, rotate_cameras=True, cut_alpha=False, format='PNG') + if state.has_density_proxy: + render_cycle(dens_proxy_transform, cycle_cams, lights, renderer, state.data_path, steps=cycle_steps, steps_per_cycle=cycle_steps, name_pre='imgP', bkg=bkg_render_alpha, img_stats=False, rotate_cameras=True, cut_alpha=False, format='PNG') + if render_shadow: + if True: + del shadow_dens + shadow_dens = dens_transform.copy_new_data(dens_transform.data *shadow_dens_scale) + render_cycle(shadow_dens, shadow_cams_cycle, shadow_lights, renderer, \ + state.data_path, steps=cycle_steps, steps_per_cycle=cycle_steps, name_pre='img_sdw', bkg=bkg_render, img_stats=False, rotate_cameras=True, format="PNG") + del shadow_dens + render_cycle(tmp_transform, [main_camera], [tf.concat([state.density.d]*3, axis=-1)], vel_renderer, state.data_path, steps=cycle_steps, steps_per_cycle=cycle_steps, name_pre='dens', img_transfer=dens_grads, img_stats=False, format="PNG") + if args.console: update_pbar(state.frame, "Density inflow, cycle") + if sF_cam and getattr(state, "target_cameras", None) is not None: + if args.console: update_pbar(state.frame, "Density, target cameras") + imgs = tf.stack(renderer.render_density_SDF_switch(dens_transform, lights, state.target_cameras, cut_alpha=False), axis=1)#, background=bkg + imgs, imgs_d = tf.split(imgs, [3,1], axis=-1) + renderer.write_images_batch_views(imgs, 'train_b{batch:04d}_cam{view:02d}', input_format="NVHWC", base_path=state.data_path, image_format='PNG') + renderer.write_images_batch_views(tf.exp(-imgs_d), 'train_trans_b{batch:04d}_cam{view:02d}', input_format="NVHWC", base_path=state.data_path, image_format='PNG') + render_cycle(tmp_transform, state.target_cameras, [tf.concat([state.density.d]*3, axis=-1)], vel_renderer, state.data_path, steps=1, steps_per_cycle=1, name_pre="train_dens", \ + img_transfer=dens_grads, img_stats=False, format="PNG") + if getattr(state, 'targets_raw', None) is not None and (len(state.target_cameras)==shape_list(state.targets_raw)[1]): + imgs_bkg = imgs +state.bkgs*tf.exp(-imgs_d) + renderer.write_images_batch_views(imgs_bkg, 'train_bkg_b{batch:04d}_cam{view:02d}', input_format="NVHWC", base_path=state.data_path, image_format='PNG') + renderer.write_images_batch_views(tf.abs(imgs_bkg - state.targets_raw), 'train_err_b{batch:04d}_cam{view:02d}', input_format="NVHWC", base_path=state.data_path, image_format='EXR') + renderer.write_images_batch_views(tf.abs(imgs - state.targets_raw), 'train_err_bkg_b{batch:04d}_cam{view:02d}', input_format="NVHWC", base_path=state.data_path, image_format='PNG') + renderer.write_images_batch_views(state.targets_raw, 'target_raw_b{batch:04d}_cam{view:02d}', input_format="NVHWC", base_path=state.data_path, image_format='PNG') + if getattr(state, 'targets', None) is not None: + renderer.write_images_batch_views(state.targets, 'target_b{batch:04d}_cam{view:02d}', input_format="NVHWC", base_path=state.data_path, image_format='PNG') + + if slices is not None: + if args.console: update_pbar(state.frame, "Density, slices") + slice_path = os.path.join(state.data_path, "slices") + os.makedirs(slice_path, exist_ok=True) + render_slices(dens_transform.data, slices, slice_path, name_pre="slc", format="EXR", normalize=False) + + if render_velocity and (state.next is not None): + vel_transform = state.get_velocity_transform() + vel_scale = vel_transform.cell_size_world().value + log.debug("Render velocity frame %d (%d) with cell size %s", state.frame, i, vel_scale) + if args.console: update_pbar(state.frame, "Velocity, main cameras") + vel_centered = state.velocity.centered() * get_vel_scale_for_render(setup, vel_transform) + val_imgs = vel_renderer.render_density(vel_transform, [tf.abs(vel_centered)], cameras, split_cameras=split_cams) + vel_renderer.write_images_batch_views(val_imgs, 'seq_velA_{batch:04d}_cam{view:02d}_{idx:04d}', base_path=setup.paths.data, frame_idx=i, image_format='PNG') + if cycle: + if args.console: update_pbar(state.frame, "Velocity, cycle") + render_cycle(vel_transform, [main_camera], [tf.abs(vel_centered)], vel_renderer, state.data_path, steps=cycle_steps, steps_per_cycle=cycle_steps, name_pre='velA', img_stats=False, format='PNG') + + # vel_mag = state.velocity.magnitude() + # max_mag = tf.reduce_max(vel_mag) + # vel_mag_grads = [(0.0, tf.constant([0,0,0], dtype=tf.float32)), + # (1.0, tf.constant([1.0,1.0,1.0], dtype=tf.float32)), + # (1.0, tf.constant([0.5,0.5,1.0], dtype=tf.float32)), + # (max_mag, tf.constant([1.0,0.0,0.0], dtype=tf.float32))] + #vel_mag = tf_element_transfer_func(vel_mag, vel_mag_grads) + if args.console: update_pbar(state.frame, "Velocity magnitude, cycle") + + vel_div = state.velocity.divergence() + vel_div = tf.concat((tf.maximum(vel_div, 0), tf.abs(vel_div), tf.maximum(-vel_div, 0)), axis=-1) + render_cycle(vel_transform, [main_camera], [vel_div], vel_renderer, state.data_path, steps=cycle_steps, steps_per_cycle=cycle_steps, name_pre='velDiv', img_stats=False, img_normalize=True, format="PNG") + del vel_div + if render_MS and state.velocity.is_MS and state.velocity.has_MS_output: + for vel_MS_scale in state.velocity.gen_current_MS_scales(): + vel_MS_shape = state.velocity.centered_shape_of_scale(vel_MS_scale) + vel_MS_centered = state.velocity.centered_MS_residual(vel_MS_scale) + vel_MS_transform = vel_transform.copy_new_data(tf.zeros([state.velocity._get_batch_size()] + vel_MS_shape + [1])) + vel_MS_centered = vel_MS_centered * get_vel_scale_for_render(setup, vel_MS_transform) + vel_MS_imgs = vel_renderer.render_density(vel_MS_transform, [tf.abs(vel_MS_centered)], cameras, split_cameras=split_cams) + vel_renderer.write_images_batch_views(vel_MS_imgs, 'seq_velAr-%03d_{batch:04d}_cam{view:02d}_{idx:04d}'%(vel_MS_shape[0],), base_path=setup.paths.data, frame_idx=i, image_format='PNG') + if False: + vel_shape = GridShape.from_tensor(vel_centered) + vel_slices_z = render_helper.image_volume_slices(vel_centered, axis=-4, abs_value=False) + vel_renderer.write_images([tf.stack(vel_slices_z, axis=0)], ['vel_slice_{:04d}'], base_path=os.path.join(state.data_path, "vel_xy"), use_batch_id=True, format='EXR') + vel_slices_x = render_helper.image_volume_slices(vel_centered, axis=-2, abs_value=False) + vel_slices_x = (tf.transpose(_, (1,0,2)) for _ in vel_slices_x) + vel_slices_x = list(tf.concat((tf.abs(_), tf.maximum(_,0), tf.maximum(-_,0)), axis=-2) for _ in vel_slices_x) + vel_renderer.write_images([tf.stack(vel_slices_x, axis=0)], ['vel_slice_{:04d}'], base_path=os.path.join(state.data_path, "vel_zy"), use_batch_id=True, format='EXR') + + i +=1 + substep = 0 + if args.console: + cycle_pbar.update(cycle_pbar._max_steps, "Done") + cycle_pbar.close() + #progress_bar(i*7,len(sequence)*7, "Frame {:03d} ({:03d}/{:03d}): {:30}".format(state.frame, i,len(sequence), "Done"), length=30) + + stop_training = False + def handle_train_interrupt(sig, frame): + global stop_training + if stop_training: + log.info('Training still stopping...') + else: + log.warning('Training interrupted, stopping...') + stop_training = True + + data_device = setup.data.resource_device #'/cpu:0' + resource_device = setup.training.resource_device #'/cpu:0' + compute_device = '/gpu:0' + log.debug("dataset device (volumes and images): %s", data_device) + log.debug("resource device (volumes and images): %s", resource_device) + log.debug("compute device: %s", compute_device) + + def wrap_resize_images(images, size): + return tf_image_resize_mip(images, size, mip_bias=0.5, method=tf.image.ResizeMethod.BILINEAR) + #return tf.image.resize_bilinear(images, size) + + # rebuild the interpolation of the active cameras here to match the target interpolation + target_cameras = build_scalarFlow_cameras(setup, [scalarFlow_cam_ids[_] for _ in setup.data.density.target_cam_ids], interpolation_weights=view_interpolation_weights) + target_cameras_base = [target_cameras[_] for _ in range(0, len(target_cameras), setup.training.density.view_interpolation.steps +1)] + + if setup.data.randomize>0: + frames = list(range(setup.data.sequence_length)) + else: + raise NotImplementedError("Direct reconstruction no longer supported.") + frames = list(range(setup.data.start, setup.data.stop, setup.data.step)) + + def _make_ImageSet(data, name, is_MS, resize_method): + if is_MS: + return ImageSetMS({scale:images for scale,images in enumerate(data)}, device=resource_device, var_name=name, resize_method=resize_method) + else: + return ImageSet(data, device=resource_device, var_name=name, resize_method=resize_method) + + def frame_loadTargets(setup, frame, sim_transform, target_dataset=None): + ''' + load or render targets + preprocessing: background subtraction, masking, view interpolation + choose targets used for visual hull + ''' + # setup targets and hulls at base_res + if not setup.data.randomize>0: + raise NotImplementedError("Direct reconstruction no longer supported.") + + loaded_data = target_dataset.frame_targets(frame, as_dict=True) + if isinstance(loaded_data, dict): + targets_raw=loaded_data["RAW"] + targets=loaded_data["PREPROC"] + bkgs=loaded_data["BKG"] + masks=loaded_data.get("MASK", None) + targets_for_hull=loaded_data["HULL"] + density=loaded_data.get("DENSITY", None) + velocity=loaded_data.get("VELOCITY", None) + transform=loaded_data.get("TRANSFORM", None) + else: + #log.info("loaded %d types of data: %s", len(loaded_data), [shape_list(_) for _ in loaded_data]) + targets_raw, targets, bkgs, targets_for_hull = loaded_data[:4] + if len(loaded_data)==5: + if shape_list(loaded_data[4])[-1]==1: + density = loaded_data[4] + else: + velocity = loaded_data[4] + elif len(loaded_data)==6: + density = loaded_data[4] + velocity = loaded_data[5] + #log.info("loaded density: %s", shape_list(density)) + transform = None + + aux = munch.Munch() + + is_MS = hasattr(target_dataset, "is_MS") and target_dataset.is_MS + + aux.targets_raw = _make_ImageSet(targets_raw, "targets_raw_f%06d"%frame, is_MS, "LINEAR") + aux.targets = _make_ImageSet(targets, "targets_f%06d"%frame, is_MS, "LINEAR") + aux.bkgs = _make_ImageSet(bkgs, "bkgs_f%06d"%frame, is_MS, "LINEAR") + if masks is not None: + aux.masks = _make_ImageSet(masks, "masks_f%06d"%frame, is_MS, "NEAREST") + else: + aux.masks = None + aux.targets_for_hull = _make_ImageSet(targets_raw, "targets_for_hull_f%06d"%frame, is_MS, "LINEAR") + aux.density = density[-1] if is_MS and density is not None else density + aux.velocity = velocity[-1] if is_MS and velocity is not None else velocity + aux.transform = transform + + return aux + + + + def get_random_target_mask(available_target_ids, *, target_weights=None, min_targets=1, max_targets=None, target_num_weights=None): + + assert min_targets>0 + if max_targets is None: max_targets=len(available_target_ids) + else: assert max_targets<=len(available_target_ids) + + #select number of targets used, based on min/max targets and the chances for an amount of targets + target_nums = list(range(min_targets, max_targets+1)) + if target_num_weights is not None: + assert len(target_num_weights)==len(target_nums) + target_num_chances_sum = sum(target_num_weights) + target_num_weights = [_/target_num_chances_sum for _ in target_num_weights] #normalize for np.choice + num_targets = np.random.choice(target_nums, p=target_num_weights) + + #select n out of m targets based on their weights + if target_weights is not None: + assert len(available_target_ids)==len(target_weights) + target_weight_sum = sum(target_weights) + target_weights = [_/target_weight_sum for _ in target_weights] #normalize for np.choice + target_ids = np.random.choice(available_target_ids, num_targets, replace=False, p=target_weights) + + return target_ids + + def state_randomize(state, randomize_input_views=False, randomize_target_views=False, randomize_transform=False, \ + transform_allow_scale=True, transform_allow_scale_non_uniform=False, transform_allow_mirror=True, transform_allow_rotY=True, transform_allow_translate=False, disable_transform_reset=False): + sequence_randomize([state], randomize_input_views, randomize_target_views, randomize_transform, \ + transform_allow_scale, transform_allow_scale_non_uniform, transform_allow_mirror, transform_allow_rotY, transform_allow_translate, disable_transform_reset=disable_transform_reset) + + def sequence_randomize(sequence, randomize_input_views=False, randomize_target_views=False, randomize_transform=False, \ + transform_allow_scale=True, transform_allow_scale_non_uniform=False, transform_allow_mirror=True, transform_allow_rotY=True, transform_allow_translate=False, disable_transform_reset=False): + #raise NotImplementedError + + if isinstance(randomize_target_views, (list, tuple)): + assert all(_ < len(setup.data.density.target_cam_ids) for _ in randomize_target_views) + target_mask = randomize_target_views + elif randomize_target_views==True: + target_mask = get_random_target_mask(list(range(len(setup.data.density.target_cam_ids))), target_weights=setup.training.randomization.target_weights, \ + min_targets=setup.training.randomization.min_targets, max_targets=setup.training.randomization.max_targets, target_num_weights=setup.training.randomization.num_targets_weights) + log.debug("Randomized target mask: %s.", target_mask) + else: + target_mask = None + for state in sequence: + state.target_mask = target_mask + + if randomize_input_views=="TARGET": + input_view_mask = target_mask + elif isinstance(randomize_input_views, (list, tuple)): + assert all(_ < len(setup.data.density.target_cam_ids) for _ in randomize_input_views) + warnings.warn("Using fixed input views {}".format(randomize_input_views)) + input_view_mask = randomize_input_views + elif randomize_input_views==True: + input_view_mask = get_random_target_mask(list(range(len(setup.data.density.target_cam_ids))), target_weights=setup.training.randomization.input_weights, \ + min_targets=setup.training.randomization.min_inputs, max_targets=setup.training.randomization.max_inputs, target_num_weights=setup.training.randomization.num_inputs_weights) + log.debug("Randomized input mask: %s.", input_view_mask) + else: + input_view_mask = None #setup.data.density.input_cam_ids ? + for state in sequence: + state.input_view_mask = input_view_mask + + if randomize_transform: + raise NotImplementedError("Use randomized transform provided by dataloader.") + if transform_allow_scale: + scale_min, scale_max = 0.94, 1.06 + if transform_allow_scale_non_uniform: + scale = np.random.uniform(scale_min, scale_max, 3).tolist() + else: + scale = [np.random.uniform(scale_min, scale_max)]*3 + else: + scale = [1,1,1] + + if transform_allow_mirror: + for dim in range(len(scale)): + flip = np.random.random()<0.5 + if flip: scale[dim] *= -1 + + if transform_allow_rotY: + #rotation = Transform.get_random_rotation() + rotation = [0,np.random.uniform(0.,360.), 0] + else: + rotation = [0,0,0] + + if transform_allow_translate: + translation_min, translation_max = state.transform.cell_size_world()*-6, state.transform.cell_size_world()*6 + translation = np.random.uniform(translation_min, translation_max).tolist() + else: + translation = [0,0,0] + + log.debug("Randomized grid transform: s=%s, r=%s, t=%s", scale, rotation, translation) + + if not disable_transform_reset or randomize_transform: + for state in sequence: + state.transform.parent.set_scale(scale if randomize_transform else None) + state.transform.parent.set_rotation_angle(rotation if randomize_transform else None) + state.transform.parent.set_translation(translation if randomize_transform else None) + + def state_set_targets(state, aux_sequence, set_size=True): + state.clear_cache() + state.base_targets_raw = aux_sequence[state.frame].targets_raw + state.base_targets = aux_sequence[state.frame].targets + state.base_bkgs = aux_sequence[state.frame].bkgs + state.base_masks = aux_sequence[state.frame].masks + + transform = aux_sequence[state.frame].get("transform", None) or sim_transform + state.transform.parent.set_scale(transform.parent.scale) + state.transform.parent.set_rotation_angle(transform.parent.rotation_deg) + state.transform.parent.set_translation(transform.parent.translation) + + set_density = (("density" in aux_sequence[state.frame]) and (aux_sequence[state.frame].density is not None) and (state.has_density) and (type(state.density)==DensityGrid)) #not isinstance(state.density, NeuralDensityGrid): + set_density_target = (("density" in aux_sequence[state.frame]) and (aux_sequence[state.frame].density is not None) and (state.has_density_target) and (type(state.density_target)==DensityGrid)) + set_velocity_target = (("velocity" in aux_sequence[state.frame]) and (aux_sequence[state.frame].velocity is not None) and (state.has_velocity_target) and (type(state.velocity_target)==VelocityGrid)) + + if set_size: + try: + res = curr_cam_res[1:] + except NameError: + log.error("Failed to get current camera resolution, using base resolution") + else: + state.base_targets_raw.resize(res) + state.base_targets.resize(res) + state.base_bkgs.resize(res) + if state.has_masks: + state.base_masks.resize(res) + + try: + res_MS = grow_handler.get_image_MS_scale_shapes() + state.base_targets_raw.create_MS_stack(res_MS) + state.base_targets.create_MS_stack(res_MS) + state.base_bkgs.create_MS_stack(res_MS) + if state.has_masks: + state.base_masks.create_MS_stack(res_MS) + except NameError: + log.exception("NameError when creating target MS stacks:") + pass + + if set_density: + #log.info("Frame %d: assinged new scaled density %s", state.frame, shape_list(aux_sequence[state.frame].density)) + state.density.assign_scaled(aux_sequence[state.frame].density) + if set_density_target: + #log.info("Frame %d: assinged new scaled density target %s", state.frame, shape_list(aux_sequence[state.frame].density)) + state.density_target.assign_scaled(aux_sequence[state.frame].density) + if set_velocity_target: #TODO + #log.info("Frame %d: assinged new scaled velocity target %s", state.frame, shape_list(aux_sequence[state.frame].density)) + state.velocity_target.assign_staggered_combined_scaled(aux_sequence[state.frame].velocity) + else: + if set_density: + #log.info("Frame %d: assinged new density %s", state.frame, shape_list(aux_sequence[state.frame].density)) + state.density.assign(aux_sequence[state.frame].density) + if set_density_target: + #log.info("Frame %d: assinged new density target %s", state.frame, shape_list(aux_sequence[state.frame].density)) + state.density_target.assign(aux_sequence[state.frame].density) + if set_velocity_target: + #log.info("Frame %d: assinged new velocity target %s", state.frame, shape_list(aux_sequence[state.frame].density)) + state.velocity_target.assign_staggered_combined(aux_sequence[state.frame].velocity) + + #log.info("state transform for frame %d: %s\n\tfrom %s", state.frame, state.transform, transform) + + def sequence_set_targets(sequence, aux_sequence, set_size=True): + for state in sequence: + state_set_targets(state, aux_sequence, set_size=set_size) + + if args.fit: + log.info("Reconstructing sequence for frames %s", frames) + setup.paths.data = setup.paths.path + os.makedirs(setup.paths.data, exist_ok=True) + try: + faultlog = open(os.path.join(setup.paths.log, 'fault.log'), 'a') + faulthandler.enable(file=faultlog) + summary = tf.contrib.summary + summary_writer = summary.create_file_writer(setup.paths.log) + + + if True: + plot_schedule(setup.training.density.learning_rate, setup.training.iterations, os.path.join(setup.paths.config, 'dens_lr.png'), 'Density LR') + plot_schedule(setup.training.velocity.learning_rate, setup.training.iterations, os.path.join(setup.paths.config, 'vel_lr.png'), 'Velocity LR') + + plot_schedule(setup.training.density.warp_loss, setup.training.iterations, os.path.join(setup.paths.config, 'dens_warp_loss.png'), 'Density Warp Loss Scale') + plot_schedule(setup.training.velocity.density_warp_loss, setup.training.iterations, os.path.join(setup.paths.config, 'vel_dens_warp_loss.png'), 'Velocity Density Warp Loss Scale') + plot_schedule(setup.training.velocity.velocity_warp_loss, setup.training.iterations, os.path.join(setup.paths.config, 'vel_vel_warp_loss.png'), 'Velocity Velocity Warp Loss Scale') + plot_schedule(setup.training.velocity.divergence_loss, setup.training.iterations, os.path.join(setup.paths.config, 'vel_div_loss.png'), 'Velocity Divergence Loss Scale') + + labels = ['Dens warp', 'Vel dens-warp', 'Vel vel-warp']#, 'Vel div'] + schedules = [setup.training.density.warp_loss, setup.training.velocity.density_warp_loss, setup.training.velocity.velocity_warp_loss]#, setup.training.velocity.divergence_loss] + plot_schedules(schedules, setup.training.iterations, os.path.join(setup.paths.config, 'warp_loss_cmp.png'), labels=labels, title='Warp Loss Comparison') + + if setup.training.discriminator.active: + plot_schedule(setup.training.discriminator.learning_rate, setup.training.iterations, os.path.join(setup.paths.config, 'disc_lr.png'), 'Discriminator LR') + + frustum_half = 0.75 + dist = 4. + log.debug("Setup validation") + val_cameras = [ + Camera(GridTransform(cam_resolution, translation=[0,0,0.8], parent=Transform(rotation_deg=[-30,0,0], parent=Transform(translation=scalarFlow_cam_focus, rotation_deg=[0,-85,0]))), nearFar=[0.3,1.3], fov=40, aspect=aspect, static=sim_transform if setup.rendering.allow_static_cameras else None), + ] + + + if len(frames)<1: + log.error("Not enough frames for sequence reconstruction: %s", frames) + sys.exit(1) + if len(frames)==1: + log.warning("Single frame reconstruction can not provide meaningfull velocity.") + + base_shape = density_size.as_shape #copy.copy(density_size) + sim_transform.grid_size = base_shape #curr_dens_shape + # print(density_size, base_shape) + + def get_max_recursive_MS_grow_levels(decoder_config, shape_cast_fn=round): + if decoder_config.recursive_MS_levels=="VARIABLE": + #return GrowingUNet.get_max_levels(density_size, scale_factor=setup.training.velocity.decoder.recursive_MS_scale_factor, min_size=setup.training.velocity.decoder.min_grid_res) + i = 0 + while (min(shape_cast_fn(_/(decoder_config.recursive_MS_scale_factor**i)) for _ in sim_transform.grid_size)>=decoder_config.min_grid_res): + i +=1 + return i + else: + return decoder_config.recursive_MS_levels + + log.info("Set up GrowHandler ...") + grow_handler = GrowHandler(base_shape=list(base_shape), base_cam_shape=train_cam_resolution, max_dens_level=get_max_recursive_MS_grow_levels(setup.training.density.decoder)-1, max_vel_level=get_max_recursive_MS_grow_levels(setup.training.velocity.decoder)-1, setup=setup) # , base_cam_factor=1 + log.warning("Image grow factor set to 1!") + log.info("GrowHandler test:\n%s", str(grow_handler)) + + + log.info("--- Pre-setup ---") + + + # TomoFluid: interpolated targets have less weight, based on the angle to a 'real' target + def get_target_weights(cameras, real_cameras, focus, mode="COS", **kwargs): + focus = Float3(focus) + # get angles from cameras to next base/real camera + angles = [] + for camera in cameras: + dir_from_focus = (Float3(camera.transform.transform(Float4(0,0,0,1)))-focus).normalized + angle = np.pi + for cam in real_cameras: + dff = (Float3(cam.transform.transform(Float4(0,0,0,1)))-focus).normalized + angle = np.minimum(np.arccos(np.dot(dir_from_focus, dff)), angle) + angles.append(angle) + + if mode=="COS": + weights = [np.cos(_*2)*0.5+0.5 for _ in angles] + # elif mode=="POW": + # min_weight = kwargs.get("min_weight", 1e-3) + # base = min_weight ** (-2./np.pi) + # weights = [base**(-_) for _ in angles] + elif mode=="EXP": + min_weight = kwargs.get("min_weight", 1e-4) + scale = np.log(min_weight) * (-2./np.pi) + weights = [np.exp(-_*scale) for _ in angles] + else: + raise ValueError("Unknown target weight mode %s"%mode) + return weights + view_interpolation_target_weights = get_target_weights(target_cameras, target_cameras_base, cam_setup.focus, mode="EXP") if setup.training.density.view_interpolation.steps>0 else None + + if isinstance(setup.data.batch_size, (list,tuple)): + assert len(setup.data.batch_size)==2 + batch_size, batch_group_size = setup.data.batch_size + assert not batch_group_size==0 + else: + batch_size = setup.data.batch_size + batch_group_size = 1 + assert batch_size>0 + log.info("Using %s batch group size %d, batch size %d.", "adaptive" if batch_group_size>0 else "fixed", abs(batch_group_size), batch_size) + + train_disc = setup.training.discriminator.active and (setup.training.discriminator.train or setup.training.discriminator.pre_opt.train or setup.training.discriminator.pre_opt.first.train) + make_disc_dataset = train_disc or setup.training.discriminator.loss_type not in ["SGAN"] + + load_density_dataset = False + load_velocity_dataset = False + SF_data_cache = None + if setup.data.randomize>0: + load_density_dataset = (not setup.training.density.decoder.active) or (setup.training.density.volume_target_loss!=0.0) #True + load_velocity_dataset = (setup.training.velocity.volume_target_loss!=0.0) #False + + if setup.data.synth_shapes.active in [False, "BOTH"] and not setup.data.SDF: + log.info("Using new SF data loader.") + target_dataset, SF_data_cache = get_targets_dataset_v2(sim_indices=setup.data.sims, frame_start=setup.data.start, frame_stop=setup.data.stop, frame_strides=setup.data.step, \ + raw=True, preproc=True, bkg=True, hull=True, batch_size=batch_size, \ + sequence_step=setup.data.sequence_step, sequence_length=setup.data.sequence_length, \ + view_indices=[scalarFlow_cam_ids[_] for _ in setup.data.density.target_cam_ids], num_views=len(setup.data.density.target_cam_ids), \ + path_raw=setup.data.density.target, path_preproc=None, SF_frame_offset=setup.data.scalarFlow_frame_offset, \ + down_scale=setup.training.train_res_down, channels=color_channel, threshold=setup.data.density.hull_threshold, shuffle_frames=True,\ + density=load_density_dataset, path_density=setup.data.density.initial_value, \ + density_t_src=sF_transform, density_t_dst=sim_transform, density_sampler=scale_renderer, \ + velocity=load_velocity_dataset, path_velocity=setup.data.velocity.initial_value, velocity_t_src=sF_transform, \ + randomize_transform=setup.training.randomization.transform, \ + cache_device=data_device, data_cache=SF_data_cache, \ + render_targets=setup.data.density.render_targets, density_renderer=synth_target_renderer, cameras=target_cameras, lights=lights, \ + density_type=setup.data.density.density_type, velocity_type=setup.data.density.density_type) + + validation_dataset, _ = get_targets_dataset_v2(sim_indices=[setup.validation.simulation], frame_start=setup.validation.start, \ + frame_stop=setup.validation.stop, frame_strides=setup.validation.step, \ + raw=True, preproc=True, bkg=True, hull=True, batch_size=setup.validation.batch_size, \ + sequence_step=get_validation_sequence_step(setup), sequence_length=setup.data.sequence_length, \ + view_indices=[scalarFlow_cam_ids[_] for _ in setup.data.density.target_cam_ids], num_views=len(setup.data.density.target_cam_ids), \ + path_raw=setup.data.density.target, path_preproc=None, SF_frame_offset=setup.data.scalarFlow_frame_offset, \ + down_scale=setup.training.train_res_down, channels=color_channel, threshold=setup.data.density.hull_threshold, shuffle_frames=True,\ + density=load_density_dataset, path_density=setup.data.density.initial_value, density_t_src=sF_transform, density_t_dst=sim_transform, density_sampler=scale_renderer, \ + velocity=load_velocity_dataset, path_velocity=setup.data.velocity.initial_value, velocity_t_src=sF_transform, \ + randomize_transform=False, \ + cache_device=data_device, data_cache=SF_data_cache, \ + render_targets=setup.data.density.render_targets, density_renderer=synth_target_renderer, cameras=target_cameras, lights=lights, \ + density_type=setup.data.density.density_type, velocity_type=setup.data.density.density_type) + + if make_disc_dataset: + log.info("Using smoke dataset for disc.") + disc_dataset, _ = get_targets_dataset_v2(sim_indices=list(range(*setup.data.discriminator.simulations)), frame_start=setup.data.discriminator.frames[0], \ + frame_stop=setup.data.discriminator.frames[1], frame_strides=setup.data.discriminator.frames[2], \ + raw=False, preproc=True, bkg=False, hull=setup.training.discriminator.conditional_hull, batch_size=setup.training.discriminator.num_real, \ + sequence_step=setup.data.sequence_step if setup.training.discriminator.temporal_input.active else 1, \ + sequence_length=3 if setup.training.discriminator.temporal_input.active else 1, \ + view_indices=[scalarFlow_cam_ids[_] for _ in setup.data.density.target_cam_ids], num_views=len(setup.data.density.target_cam_ids), \ + path_raw=setup.data.density.target, path_preproc=None, SF_frame_offset=setup.data.scalarFlow_frame_offset, \ + down_scale=setup.data.discriminator.real_res_down, channels=color_channel, threshold=setup.data.density.hull_threshold, shuffle_frames=True,\ + density=False, path_density=setup.data.discriminator.initial_value, density_t_src=sF_transform, density_t_dst=sim_transform, density_sampler=scale_renderer, \ + velocity=False, path_velocity=None, velocity_t_src=sF_transform, \ + randomize_transform=False, \ + cache_device=data_device, data_cache=SF_data_cache if setup.data.discriminator.render_targets==setup.data.density.render_targets else None, \ + render_targets=setup.data.discriminator.render_targets, density_renderer=synth_target_renderer, cameras=target_cameras, lights=lights, \ + density_type=setup.data.discriminator.density_type, velocity_type=setup.data.discriminator.density_type) + + if setup.data.synth_shapes.active=="BOTH": + SF_target_dataset = target_dataset + del target_dataset + if make_disc_dataset: + SF_disc_dataset = disc_dataset + del disc_dataset + + if setup.data.synth_shapes.active in [True,"BOTH"] or setup.data.SDF: + def get_max_cell_size(): + min_grid_res = [setup.training.velocity.decoder.min_grid_res, int(math.ceil(setup.training.velocity.decoder.min_grid_res * setup.data.y_scale)), setup.training.velocity.decoder.min_grid_res] + tmp_T = sim_transform.copy_no_data() + tmp_T.grid_size = min_grid_res + max_cell_size = tmp_T.cell_size_world() + return min(max_cell_size) + synth_max_cell_size = get_max_cell_size() #cell size at coarsest resolution, as defined by min_grid_res + synth_max_translation = synth_max_cell_size * setup.data.synth_shapes.max_translation + is_dens_preopt = True #setup.training.density.pre_optimization # larger objects/shapes + log.info("Using synthetic shape dataset. Max cell size: %f", synth_max_cell_size) + if is_dens_preopt: log.info("Using larger shapes.") + def _get_dataset_transform(): + if setup.data.MS_volume: + transforms = [] + for interval in grow_handler.main_dens_schedule.intervals: + t = copy.deepcopy(sim_transform) + t.grid_size = interval.value + transforms.append(t) + return transforms + else: + return sim_transform + def _get_dataset_cameras(): + if setup.data.MS_images: + scales_cameras = [] + for interval in grow_handler.main_cam_schedule.intervals: + cameras = copy.deepcopy(target_cameras) + for camera in cameras: + camera.transform.grid_size = interval.value + scales_cameras.append(cameras) + return scales_cameras + else: + return target_cameras + log.info("Using new Synth data loader/generator.") + #sample_overrides = {'density_scale': 0.2, "shape_type":setup.data.synth_shapes.shape_types, "initial_translation":[0,0,0]} #, "base_scale":[0.35]*3,"initial_rotation_rotvec":[0,0,0], "rotvec":[0,0,0], 'density_scale': 0.2 + sample_overrides = {"shape_type":setup.data.synth_shapes.shape_types,} #, "base_scale":[0.35]*3,"initial_rotation_rotvec":[0,0,0], "rotvec":[0,0,0], 'density_scale': 0.2 + if setup.data.synth_shapes.init_center: + log.info("Centered initial position .") + sample_overrides["initial_translation"]=[0,0,0] + + SDF_dataset_cache = None + SDF_frames = [] + if setup.data.synth_shapes.active==False: + SDF_dataset_cache = SDFDatasetCache(path_mask=setup.data.density.initial_value, device=data_device) + SDF_frames = list(range(setup.data.start, setup.data.stop, setup.data.step)) + + target_dataset = get_synthTargets_dataset_v2(batch_size=batch_size, base_grid_transform=_get_dataset_transform(), sequence_length=setup.data.sequence_length, \ + view_indices=[scalarFlow_cam_ids[_] for _ in setup.data.density.target_cam_ids], num_views=len(setup.data.density.target_cam_ids), \ + cameras=_get_dataset_cameras(), lights=lights, device=resource_device, \ + density_range=[0.2,0.4] if is_dens_preopt else [0.05,0.14], \ + inner_range=[0.0,0.4], \ + scale_range=[0.25,0.35] if setup.data.SDF else [synth_max_cell_size,0.5], #if is_dens_preopt else [synth_max_cell_size,synth_max_cell_size*2.0], + translation_range=[-synth_max_translation,synth_max_translation], \ + rotation_range=[10,30], #[0,20], + raw=True, preproc=True, bkg=True, hull=True, mask=setup.data.SDF, channels=1, SDF=setup.data.SDF, \ + density=load_density_dataset, velocity=load_velocity_dataset, advect_density=load_velocity_dataset, \ + density_sampler=density_sampler, density_renderer=synth_target_renderer, randomize_transform=setup.training.randomization.transform, \ + seed=np.random.randint(np.iinfo(np.int32).max), sample_overrides=sample_overrides, \ + data_cache=SDF_dataset_cache, generate_shape=setup.data.synth_shapes.active, \ + generate_sequence=setup.data.synth_shapes.active or len(SDF_frames)<2 , \ + sims=setup.data.sims, frames=SDF_frames, steps=setup.data.sequence_step) + + if not setup.data.synth_shapes.active=="BOTH" or setup.data.SDF: + SDF_val_frames = list(range(setup.validation.start, setup.validation.stop, setup.validation.step)) + val_sample_overrides = copy.copy(sample_overrides) + val_sample_overrides.update({"shape_type":setup.validation.synth_data_shape_types}) + validation_dataset = get_synthTargets_dataset_v2(batch_size=setup.validation.batch_size, base_grid_transform=_get_dataset_transform(), sequence_length=setup.data.sequence_length, \ + view_indices=[scalarFlow_cam_ids[_] for _ in setup.data.density.target_cam_ids], num_views=len(setup.data.density.target_cam_ids), \ + cameras=_get_dataset_cameras(), lights=lights, device=resource_device, \ + density_range=[0.15,0.25] if is_dens_preopt else [0.05,0.14], \ + inner_range=[0.1,0.4], \ + scale_range=[0.25,0.35] if setup.data.SDF else [0.3,0.5], # if is_dens_preopt else [synth_max_cell_size,synth_max_cell_size*2.0], + translation_range=[-synth_max_translation,synth_max_translation], \ + rotation_range=[10,30], #[0,20], + raw=True, preproc=True, bkg=True, hull=True, mask=setup.data.SDF, channels=1, SDF=setup.data.SDF, \ + density=load_density_dataset, velocity=load_velocity_dataset, advect_density=load_velocity_dataset, \ + density_sampler=density_sampler, density_renderer=synth_target_renderer, randomize_transform=False, \ + seed=np.random.randint(np.iinfo(np.int32).max) if setup.validation.synth_data_seed is None else setup.validation.synth_data_seed, \ + sample_overrides=val_sample_overrides, # if is_dens_preopt else 0.1,, "base_scale":[0.35]*3 if is_dens_preopt else [0.18]*3, "initial_translation":[0,0,0] + data_cache=SDF_dataset_cache, generate_shape=setup.data.synth_shapes.active, \ + generate_sequence=setup.data.synth_shapes.active or len(SDF_val_frames)<2 , \ + sims=[setup.validation.simulation], frames=SDF_val_frames, steps=setup.data.sequence_step) + else: + synth_target_dataset = target_dataset + del target_dataset + + if make_disc_dataset: + SDF_disc_frames = list(range(*setup.data.discriminator.frames)) + disc_dataset = get_synthTargets_dataset_v2(batch_size=setup.training.discriminator.num_real, base_grid_transform=_get_dataset_transform(), \ + sequence_length=3 if setup.training.discriminator.temporal_input.active else 1, \ + view_indices=[scalarFlow_cam_ids[_] for _ in setup.data.density.target_cam_ids], num_views=len(setup.data.density.target_cam_ids), \ + cameras=_get_dataset_cameras(), lights=lights, device=resource_device, \ + density_range=[0.12,0.35] if is_dens_preopt else [0.05,0.14], \ + inner_range=[0.1,0.4], \ + scale_range=[0.25,0.35] if setup.data.SDF else [synth_max_cell_size,0.5], #if is_dens_preopt else [synth_max_cell_size,synth_max_cell_size*2.0], + translation_range=[-synth_max_translation,synth_max_translation], \ + rotation_range=[10,30], #[0,20], + raw=False, preproc=True, bkg=False, hull=setup.training.discriminator.conditional_hull, mask=False, channels=1, SDF=setup.data.SDF, \ + density=False, velocity=False, advect_density=load_velocity_dataset, \ + density_sampler=density_sampler, density_renderer=synth_target_renderer, randomize_transform=False, \ + seed=np.random.randint(np.iinfo(np.int32).max), sample_overrides=sample_overrides, \ + data_cache=SDF_dataset_cache, generate_shape=setup.data.synth_shapes.active, \ + generate_sequence=setup.data.synth_shapes.active or len(SDF_disc_frames)<2 , \ + sims=list(range(*setup.data.discriminator.simulations)), frames=SDF_disc_frames, \ + steps=setup.data.sequence_step if setup.training.discriminator.temporal_input.active else [1]) + + if setup.data.synth_shapes.active=="BOTH" and not setup.data.SDF: + target_dataset = MultiDataset((SF_target_dataset, synth_target_dataset), weights=(0.5,0.5), seed=np.random.randint(np.iinfo(np.int32).max)) + if make_disc_dataset: + disc_dataset = MultiDataset((SF_disc_dataset, disc_dataset), weights=(0.5,0.5), seed=np.random.randint(np.iinfo(np.int32).max)) + + # returns: [NSVHWC for type] + if False and setup.data.synth_shapes.active: + target_dataset = TargetDataset(target_data, resource_device=resource_device) + validation_dataset = TargetDataset(validation_data, resource_device=resource_device) + + + aux_sequence = {} + val_sequence = {} + # if setup.data.clip_grid: + log.info("Load targets") + for frame in frames: + aux_sequence[frame] = frame_loadTargets(setup, frame, sim_transform, target_dataset) + val_sequence[frame] = frame_loadTargets(setup, frame, sim_transform, validation_dataset) + + + if not setup.data.randomize>0: + raise NotImplementedError + else: + log.info("%s, cell size %s, grid size %s from %s to %s", sim_transform, sim_transform.cell_size_world(), sim_transform.grid_size_world(), sim_transform.grid_min_world(), sim_transform.grid_max_world()) + + + curr_cam_res = current_grow_shape(train_cam_resolution, 0, setup.training.density.grow.factor, setup.training.density.grow.intervals) + main_opt_start_dens_shape = current_grow_shape(base_shape, 0, setup.training.density.grow.factor, setup.training.density.grow.intervals) + main_opt_start_vel_shape = current_grow_shape(base_shape, 0, setup.training.velocity.grow.factor, setup.training.velocity.grow.intervals) + pre_opt_start_dens_shape = copy.deepcopy(main_opt_start_dens_shape) + pre_opt_start_vel_shape = current_grow_shape(main_opt_start_vel_shape, 0, setup.training.velocity.pre_opt.grow.factor, setup.training.velocity.pre_opt.grow.intervals) + curr_dens_shape = pre_opt_start_dens_shape if setup.training.density.pre_optimization else main_opt_start_dens_shape + curr_vel_shape = pre_opt_start_vel_shape if setup.training.velocity.pre_optimization else main_opt_start_vel_shape + z = tf.zeros([1] + curr_vel_shape + [1]) + log.info("Inital setup for sequence reconstruction:\n\tbase shape %s,\n\tinitial density shape %s,\n\tinitial render shape %s,\n\tpre-opt velocity shape %s,\n\tinitial velocity shape %s", \ + base_shape, main_opt_start_dens_shape, curr_cam_res, pre_opt_start_vel_shape, main_opt_start_vel_shape) + + vel_bounds = None if setup.data.velocity.boundary.upper()=='CLAMP' else \ + Zeroset(-1, shape=density_size, outer_bounds="OPEN" if setup.data.velocity.boundary.upper()=='CLAMP' else 'CLOSED', as_var=False, device=resource_device) + + ### NETWORK SETUP ### + + + def setup_decoder(decoder_config, in_channels, out_channels, shape, name="Decoder", **kwargs): + dim = len(shape) #"LIFTING" if "lifting_shape" in kwargs else + if "lifting_shape" in kwargs: + dim = "LIFTING" + lifting_shape = kwargs["lifting_shape"] + shape = shape if min(lifting_shape)>min(shape) else lifting_shape + if (not hasattr(decoder_config.model, "num_levels")) or decoder_config.model.num_levels=="VARIABLE": + num_levels = GrowingUNet.get_max_levels(shape, scale_factor=decoder_config.model.level_scale_factor, min_size=decoder_config.min_grid_res, allow_padded=dim in [2, "LIFTING"]) + else: + num_levels = decoder_config.model.num_levels + log.info("%s with %d levels for %s", name, num_levels, shape) + net_args = copy.deepcopy(decoder_config.model) + if hasattr(net_args, "num_levels"): del net_args["num_levels"] + decoder_model = GrowingUNet( dimension=dim, num_levels=num_levels, input_channels=in_channels, \ + output_levels=1, output_channels=out_channels, name=name, \ + **net_args, **kwargs) + decoder_model.set_active_level(decoder_config.start_level) + decoder_model.set_train_mode(decoder_config.train_mode, schedule=make_schedule(decoder_config.train_mode_schedule)) + decoder_model.skip_merge_weight_schedule = make_schedule(decoder_config.skip_merge_weight_schedule) + if not decoder_config.recursive_MS: + decoder_model.set_grow_intervals(decoder_config.grow_intervals) + + string_buffer = StringWriter() + string_buffer.write_line(name) + string_buffer.write_line(" Model:") + decoder_model.summary(print_fn=string_buffer.write_line) + log.info(string_buffer.get_string()) + string_buffer.reset() + return decoder_model + + def load_decoder(path, decoder_config, in_channels, shape, name="Decoder", **kwargs): + assert isinstance(path, str) + dim = len(shape) + if "lifting_shape" in kwargs: + dim = "LIFTING" + lifting_shape = kwargs["lifting_shape"] + shape = shape if min(lifting_shape)>min(shape) else lifting_shape + scale_factor = decoder_config.recursive_MS_scale_factor if "recursive_MS_scale_factor" in decoder_config else 2.0 + num_levels = GrowingUNet.get_max_levels(shape, scale_factor=scale_factor, min_size=decoder_config.min_grid_res, allow_padded=dim in [2, "LIFTING"]) + # load model from checkpoint + decoder_model = load_model(path, num_levels=num_levels, input_merge_weight=0.5, skip_merge_weight=1.0, **kwargs) + + if isinstance(decoder_model, RWDensityGeneratorNetwork): + log.info("Loaded RWDensityGeneratorNetwork from '%s'", path) + elif isinstance(decoder_model, RWVelocityGeneratorNetwork): + log.info("Loaded RWVelocityGeneratorNetwork from '%s'", path) + else: + log.info("Loaded %s from '%s' with %d levels for %s", name, path, decoder_model.num_levels, shape) + if decoder_model.input_channels!=in_channels: + log.error("Loaded model input channels (%d) not compatible with current setup (%d).", decoder_model.input_channels, in_channels) + sys.exit(1) + + string_buffer = StringWriter() + string_buffer.write_line(name) + string_buffer.write_line(" Model:") + decoder_model.summary(print_fn=string_buffer.write_line) + log.info(string_buffer.get_string()) + string_buffer.reset() + return decoder_model + + + + def get_gen_num_inputs(decoder_config, feature_channels, out_channels): + num_inputs = 0 + num_inputs += len(decoder_config.step_input_density) + num_inputs += len(decoder_config.step_input_density_target) + if hasattr(decoder_config, "step_input_density_proxy"): + num_inputs += len(decoder_config.step_input_density_proxy) + volume_encoder_channels = color_channel + if setup.training.view_encoder.lifting=="UNPROJECT" and setup.training.view_encoder.merge in ["NETWORK_CONCAT", "CONCAT_NETWORK"]: + volume_encoder_channels = volume_encoder_model.output_channels + if setup.training.randomization.inputs==True: + raise ValueError("View concatentaion does not work with randomized input views.") + elif isinstance(setup.training.randomization.inputs, (list,tuple)) and setup.training.view_encoder.merge=="NETWORK_CONCAT": + volume_encoder_channels *= len(setup.training.randomization.inputs) + elif not setup.training.view_encoder.merge=="CONCAT_NETWORK": + volume_encoder_channels *= len(target_cameras) + elif setup.training.view_encoder.lifting=="NETWORK": + volume_encoder_channels = lifting_network_model.output_channels + + num_views = None + if isinstance(setup.training.randomization.inputs, (list,tuple)): + num_views = len(setup.training.randomization.inputs) + elif setup.training.randomization.inputs==False: + num_views = len(target_cameras) + + num_inputs += len(decoder_config.step_input_features) * NeuralState.get_base_feature_channels(decoder_config.type_input_features, color_channels=color_channel, volume_encoder_channels=volume_encoder_channels, num_views=num_views) + if "ENCLIFT" in decoder_config.type_input_features: + num_inputs += len(decoder_config.step_input_features) * vel_input_encoder_channels + if setup.data.SDF and decoder_config.get("base_SDF_mode", "NONE")=="INPUT_RESIDUAL": + num_inputs += 1 + if decoder_config.recursive_MS: num_inputs +=out_channels + return num_inputs + + def get_decoder_model(decoder_config, out_channels, shape, name="Decoder", in_channels=None, **kwargs): + is_load_model = False + if isinstance(decoder_config.model, str): + # load model from file + model_path = run_index[decoder_config.model] #setup.training.density.decoder.model] + if model_path is None: + model_path = decoder_config.model + model_paths = get_NNmodel_names(model_path) + is_load_model = True + + if in_channels is None: + input_channels = get_gen_num_inputs(decoder_config, view_encoder_channels, out_channels) + else: + input_channels = in_channels + log.info("%s input channels: %d", name, input_channels) + if decoder_config.recursive_MS and not decoder_config.recursive_MS_shared_model: + # setup separate model for each level + max_levels = get_max_recursive_MS_grow_levels(decoder_config) + decoder_model = [] + if is_load_model: + if len(model_paths)==1: #loading a shared model + model_paths = model_paths*max_levels + log.info("Loading single decoder for recursive multi-scale with %d levels.", max_levels) + elif len(model_paths)!=max_levels: + log.error("Trying to load a non-shared model from file, but %d models are available for mask '%s', %d models are needed.", len(model_paths), decoder_config.model, max_levels) + sys.exit(1) + else: + log.info("Loading %d decoders for recursive multi-scale.", max_levels) + for level in range(max_levels): + decoder_model.append(load_decoder(model_paths[level], decoder_config, input_channels, name="{}_L{:03d}".format(name,level), shape=shape, **kwargs)) + else: + log.info("Setup %d decoders for recursive multi-scale.", max_levels) + for level in range(max_levels): + decoder_model.append(setup_decoder(decoder_config, input_channels, out_channels, name="{}_L{:03d}".format(name, level), shape=shape, **kwargs)) + else: + if is_load_model: + if len(model_paths)>1: + log.error("Trying to load a shared model from file, but %d models are available for mask '%s': %s", len(model_paths), decoder_config.model, model_paths) + sys.exit(1) + decoder_model = load_decoder(model_paths[0], decoder_config, input_channels, name=name, shape=shape, **kwargs) + else: + decoder_model = setup_decoder(decoder_config, input_channels, out_channels, name=name, shape=shape, **kwargs) + + return decoder_model + + + setup.training.view_encoder.encoder = set(setup.training.view_encoder.encoder) + def setup_target_encoder(): + view_encoder_channels = 0 + target_encoder_model = None + if "NETWORK" in setup.training.view_encoder.encoder: + view_encoder_config = copy.deepcopy(setup.training.view_encoder) + view_encoder_config.recursive_MS = False + del view_encoder_config.model["output_channels"] + target_encoder_model = get_decoder_model(view_encoder_config, in_channels=color_channel, \ + out_channels=setup.training.view_encoder.model.output_channels, name="ViewEncoder", shape=train_cam_resolution[1:]) + + if target_encoder_model.num_levels>1: + raise NotImplementedError("handle skip merge weights, default to 0.0.") + + view_encoder_channels += setup.training.view_encoder.model.output_channels + + if "L" in setup.training.view_encoder.encoder: + view_encoder_channels += 1 + + if "IDENTITY" in setup.training.view_encoder.encoder: + view_encoder_channels += color_channel #1 if setup.rendering.monochrome else 3 + + if view_encoder_channels<1: + raise ValueError("Empty input encoder.") + return target_encoder_model, view_encoder_channels + + def setup_lifting_network(): + if setup.training.view_encoder.lifting.upper()=="NETWORK": + if setup.training.lifting_network.active: + if isinstance(setup.training.lifting_network.model, str) and setup.training.lifting_network.model.startswith("SDFDiff"): + log.info("Using SDFDiff lifting network.") + if not (setup.data.grid_size==64 and len(setup.training.view_encoder.encoder)==1 and 'IDENTITY' in setup.training.view_encoder.encoder and isinstance(setup.training.randomization.inputs, list) and len(setup.training.randomization.inputs) in [1,2]): + raise RuntimeError("Wrong configuration for SDFDiff lifting network") + lifting_network_model = SDFDiffAENetwork(input_channels=color_channel) + + if setup.training.lifting_network.model.startswith("SDFDiff=[RUNID:"): + # my tf/keras version seems broken regarding loading full models, so load weights instead + model_path = setup.training.lifting_network.model[8:] + model_path = run_index[model_path] or model_path + model_paths = get_NNmodel_names(model_path) + assert len(model_paths)==1 + model_path = model_paths[0] + "_model.h5" + log.info("Loading weights from: %s", model_path) + lifting_network_model.load_weights(model_path) + + string_buffer = StringWriter() + string_buffer.write_line("SDFDiff Lifting Network Model:") + lifting_network_model.summary(print_fn=string_buffer.write_line) + log.info(string_buffer.get_string()) + string_buffer.reset() + + else: + log.warning("Using lifting UNet.") + assert len(setup.training.randomization.inputs)==1 + lifting_cameras = [target_cameras[_] for _ in setup.training.randomization.inputs] + if not setup.training.randomization.inputs==setup.validation.input_view_mask: + raise NotImplementedError("TODO: Set lifting_network's cameras during training and validation") + lifting_transform = sim_transform.copy_no_data() + + lifting_network_config = copy.deepcopy(setup.training.lifting_network) + lifting_network_config.recursive_MS = False + out_channels = 0 + if isinstance(lifting_network_config.model, dict): + out_channels = setup.training.lifting_network.model.output_channels + del lifting_network_config.model["output_channels"] + lifting_network_model = get_decoder_model(lifting_network_config, in_channels=view_encoder_channels, \ + out_channels=out_channels, name="LiftingNetwork", \ + shape=train_cam_resolution[1:], lifting_renderer=lifting_renderer, lifting_cameras=lifting_cameras, lifting_transform=lifting_transform, lifting_shape=density_size.as_shape.tolist(), \ + enc_outputs="ENCLIFT" in setup.training.velocity.decoder.type_input_features) + + skip_merge_weight = 1.0 + log.info("Setting skip merge weights to %f.", skip_merge_weight) + for l in range(1, lifting_network_model.num_levels): + lifting_network_model.set_skip_merge_weight(skip_merge_weight,l) + else: + raise RuntimeError + # old fully connection version + log.warning("Experimental lifting network active!") + # fixed shapes for testing + image_shape = [33,18,3] + volume_shape = [11,22,11,setup.training.volume_encoder.model.output_channels] + lifting_network_model = LiftingNetwork(input_shape=image_shape, output_shape=volume_shape) + + string_buffer = StringWriter() + string_buffer.write_line("Lifting Network Model:") + lifting_network_model.summary(print_fn=string_buffer.write_line) + log.info(string_buffer.get_string()) + string_buffer.reset() + + return lifting_network_model + else: + return None + + + volume_encoder_model = None + lifting_network_model = None + if (setup.training.density.decoder.active or setup.training.velocity.decoder.active): + target_encoder_model, view_encoder_channels = setup_target_encoder() + + if setup.training.view_encoder.lifting.upper()=="UNPROJECT" and setup.training.volume_encoder.active: + def get_volume_encoder_in_channels(): + c = view_encoder_channels + if setup.training.view_encoder.lifting=="UNPROJECT" and setup.training.view_encoder.merge=="CONCAT_NETWORK": + if isinstance(setup.training.randomization.inputs, list): + c *= len(setup.training.randomization.inputs) + elif setup.training.randomization.inputs in [True, "TARGET"]: + raise ValueError("View concatentaion does not work with randomized input views.") + else: + c *= len(target_cameras) + return c + def get_volume_encoder_out_channels(): + if isinstance(setup.training.volume_encoder.model, str): + # support for loading models + model_path = run_index[setup.training.volume_encoder.model] #setup.training.density.decoder.model] + if model_path is None: + model_path = setup.training.volume_encoder.model + + model_path += ".json" + if not os.path.exists(model_path): + raise IOError("Can't read config for volume encoder model: file '%s' not found."%(model_path,)) + + with open(model_path, "r") as config_file: + config = json.load(config_file) + return config["_config"]["output_channels"] + + else: + return setup.training.volume_encoder.model.output_channels + volume_encoder_config = copy.deepcopy(setup.training.volume_encoder) + volume_encoder_config.recursive_MS = False + if isinstance(volume_encoder_config.model, dict): + del volume_encoder_config.model["output_channels"] + + volume_encoder_model = get_decoder_model(volume_encoder_config, in_channels=get_volume_encoder_in_channels(), \ + out_channels=get_volume_encoder_out_channels(), shape=density_size, name="VolumeEncoder") + + vol_enc_skip_merge_weight = 1.0 + log.info("Setting volume encoder skip merge weights to %f.", vol_enc_skip_merge_weight) + for l in range(1, volume_encoder_model.num_levels): + volume_encoder_model.set_skip_merge_weight(vol_enc_skip_merge_weight,l) + elif setup.training.view_encoder.lifting.upper()=="NETWORK": + lifting_network_model = setup_lifting_network() + + def get_vel_input_encoder(): + vel_input_encoder = None + out_channels = 0 + if "ENCLIFT" in setup.training.velocity.decoder.type_input_features: + # take skip lifting output of state lifting encoder + # make sure it exisits + if lifting_network_model is None: raise RuntimeError("Can't use ENCLIFT input without lifting network.") + if not setup.training.velocity.decoder.recursive_MS_shared_model: raise NotImplementedError + # check if shared vel input encoders are possibel - if lifting network is shared. + if not lifting_network_model.share_encoder: + is_load_model = False + if isinstance(setup.training.velocity.decoder.model, str): + raise NotImplementedError + is_load_model = True + decoder_config = { + "model": { + "num_levels": 1, + "level_scale_factor": 2.0, + + "input_levels": 1, + "create_inputs": False, + "input_blocks": [], + "share_input_layer": True, + + "down_mode": "NONE", # STRIDED, NONE + "down_conv_filters": None, + "down_conv_kernel_size": 4, + "share_down_layer": True, + "encoder_resblocks": [], + "share_encoder": True, + + "decoder_resblocks": [], + "share_decoder": False, + "up_mode": "NNSAMPLE_CONV", + "up_conv_filters": 16, + "up_conv_kernel_size": 4, + "share_up_layer": True, + + "skip_merge_mode": "CONCAT", # CONCAT, WSUM, SUM + + #"output_blocks": ["C:16-1"], + "output_blocks": [], + "share_output_layer": True, + "output_conv_kernel_size": 1, #0 to disable additional output convolution + "output_activation": "relu", + "output_mode": "SINGLE", # SINGLE, RESIDUAL + "conv_padding": "ZERO", # ZERO, MIRROR + }, + "recursive_MS": True, + #"min_grid_res": 4,#8, + "start_level": 0, + "train_mode": "ALL", #ALL, TOP + "train_mode_schedule": False, #ALL, TOP + "skip_merge_weight_schedule": 1.0, + + "recursive_MS_levels": lifting_network_model.num_levels, + #"recursive_MS_direct_input": False, + #"recursive_MS_scale_factor": dec_scale_factor, + "recursive_MS_shared_model": False, + "recursive_MS_train_mode": "ALL", #ALL, TOP + #"recursive_MS_copy_on_grow": False, + #"grow_intervals": [], + } + decoder_config = munch.munchify(decoder_config) + out_channels = 8 + + vel_input_encoder = [] + if is_load_model: + raise NotImplementedError + else: + log.info("Setup %d velocity input encoders.", lifting_network_model.num_levels) + for level in range(lifting_network_model.num_levels): + vel_input_encoder.append(setup_decoder(decoder_config, lifting_network_model._get_encoder_output_channels(level), out_channels, name="VelocityInpEnc_L{:03d}".format(level), shape=density_size)) + else: + vel_input_encoder = None + out_channels = lifting_network_model._get_encoder_output_channels(0) + # check if required scales are available + + return vel_input_encoder, out_channels + + def get_vel_downscale_encoder(): + vel_downscale_encoder = None + channels = 0 + + if "ENCODER" in setup.training.velocity.decoder.downscale_input_modes: + is_load_model = False + if isinstance(setup.training.velocity.decoder.model, str): + raise NotImplementedError + is_load_model = True + + assert setup.training.velocity.decoder.recursive_MS_scale_factor==2 + + decoder_config = { + "model": { + "num_levels": 1, + "level_scale_factor": 2.0, + + "input_levels": 1, + "create_inputs": False, + "input_blocks": ["C:16-4-s2", "C:16-3"], #2x down + "share_input_layer": True, + + "down_mode": "NONE", # STRIDED, NONE + "down_conv_filters": None, + "down_conv_kernel_size": 4, + "share_down_layer": True, + "encoder_resblocks": [], + "share_encoder": True, + + "decoder_resblocks": [], + "share_decoder": False, + "up_mode": "NNSAMPLE_CONV", + "up_conv_filters": 16, + "up_conv_kernel_size": 4, + "share_up_layer": True, + + "skip_merge_mode": "CONCAT", # CONCAT, WSUM, SUM + + #"output_blocks": ["C:16-1"], + "output_blocks": [], + "share_output_layer": True, + "output_conv_kernel_size": 3, #0 to disable additional output convolution + "output_activation": "relu", + "output_mode": "SINGLE", # SINGLE, RESIDUAL + "conv_padding": "ZERO", # ZERO, MIRROR + }, + "recursive_MS": True, + #"min_grid_res": 4,#8, + "start_level": 0, + "train_mode": "ALL", #ALL, TOP + "train_mode_schedule": False, #ALL, TOP + "skip_merge_weight_schedule": 1.0, + + "recursive_MS_levels": lifting_network_model.num_levels, + #"recursive_MS_direct_input": False, + #"recursive_MS_scale_factor": dec_scale_factor, + "recursive_MS_shared_model": False, + "recursive_MS_train_mode": "ALL", #ALL, TOP + #"recursive_MS_copy_on_grow": False, + #"grow_intervals": [], + } + decoder_config = munch.munchify(decoder_config) + + vel_levels = None + channels = view_encoder_channels + + if setup.training.velocity.decoder.share_downscale_encoder: + if is_load_model: + raise NotImplementedError + else: + log.info("Setup shared velocity input downscale encoder.") + vel_downscale_encoder = setup_decoder(decoder_config, channels, channels, name="VelocityDownEnc", shape=density_size) + else: + if is_load_model: + raise NotImplementedError + else: + log.info("Setup %d velocity input downscale encoders.", lifting_network_model.num_levels) + vel_downscale_encoder = [] + for level in range(lifting_network_model.num_levels-1): # no network needed for highest level + vel_downscale_encoder.append(setup_decoder(decoder_config, channels, channels, name="VelocityDownEnc_L{:03d}".format(level), shape=density_size)) + + return vel_downscale_encoder, channels + + if setup.training.velocity.decoder.active: + vel_input_encoder_model, vel_input_encoder_channels = get_vel_input_encoder() + + # actually a vel input downscale network + vel_downscale_encoder_model, vel_downscale_encoder_channels = get_vel_downscale_encoder() + + if setup.training.velocity.decoder.model=="RW": + log.warning("Using RW velocity network.") + if not (setup.data.grid_size==64 and setup.training.velocity.decoder.step_input_density==[0] and setup.training.velocity.decoder.step_input_density_proxy==[1] and setup.training.velocity.decoder.step_input_features==[]): + raise RuntimeError("Wrong configuration for RW velocity network") + velocity_decoder_model = RWVelocityGeneratorNetwork(dens_channels=1, unp_channels=1, use_proxy=True) + + string_buffer = StringWriter() + string_buffer.write_line("RW velocity Network Model:") + velocity_decoder_model.summary(print_fn=string_buffer.write_line) + log.info(string_buffer.get_string()) + string_buffer.reset() + + else: + velocity_decoder_model = get_decoder_model(setup.training.velocity.decoder, 3, shape=density_size, name="VelocityDecoder") + if velocity_decoder_model.num_levels>1: + skip_merge_weight = 1.0 + log.info("Setting skip merge weights to %f.", skip_merge_weight) + for l in range(1, velocity_decoder_model.num_levels): + velocity_decoder_model.set_skip_merge_weight(skip_merge_weight,l) + + + if setup.training.density.decoder.active: + if setup.training.density.decoder.model=="SDFDiff": + log.warning("Using SDFDiff refinement network.") + if not (setup.data.grid_size==64 and isinstance(lifting_network_model, SDFDiffAENetwork)): + raise RuntimeError("Wrong configuration for SDFDiff refinement network") + density_decoder_model = SDFDiffRefinerNetwork() + + string_buffer = StringWriter() + string_buffer.write_line("SDFDiff Refinement Network Model:") + density_decoder_model.summary(print_fn=string_buffer.write_line) + log.info(string_buffer.get_string()) + string_buffer.reset() + + elif setup.training.density.decoder.model=="RW": + log.warning("Using RW density network.") + if not (setup.data.grid_size==64 and len(setup.training.view_encoder.encoder)==1 and 'L' in setup.training.view_encoder.encoder and setup.training.view_encoder.lifting=="UNPROJECT" and setup.training.view_encoder.merge=="CONCAT" and isinstance(setup.training.randomization.inputs, list) and len(setup.training.randomization.inputs) in [1,2]): + raise RuntimeError("Wrong configuration for RW density network") + single_view = len(setup.training.randomization.inputs)==1 + density_decoder_model = RWDensityGeneratorNetwork(input_channels=1, w1=(1.0 if single_view else 0.5), w2=(0 if single_view else 0.5)) + del single_view + string_buffer = StringWriter() + string_buffer.write_line("SDFDiff Refinement Network Model:") + density_decoder_model.summary(print_fn=string_buffer.write_line) + log.info(string_buffer.get_string()) + string_buffer.reset() + + else: + density_decoder_model = get_decoder_model(setup.training.density.decoder, 1, shape=density_size, name="DensityDecoder")#, in_channels=9) + if isinstance(density_decoder_model, GrowingUNet) and density_decoder_model.num_levels>1: + raise NotImplementedError("handle skip merge weights, default to 0.0.") + + # new network, create after the others to keep initialization consistent with previous tests + def setup_frame_merge_network(): + if (setup.training.density.decoder.active or setup.training.velocity.decoder.active) and setup.training.frame_merge_network.active: + volume_encoder_channels = volume_encoder_model.output_channels if volume_encoder_model is not None else lifting_network_model.output_channels + frame_merge_network_config = copy.deepcopy(setup.training.frame_merge_network) + frame_merge_network_config.recursive_MS = False + frame_merge_network_model = get_decoder_model(frame_merge_network_config, in_channels=volume_encoder_channels*2, \ + out_channels=volume_encoder_channels, shape=density_size, name="FrameMerge") + if frame_merge_network_model.num_levels>1: + raise NotImplementedError("handle skip merge weights, default to 0.0.") + return frame_merge_network_model + else: + return None + frame_merge_network_model = setup_frame_merge_network() + + + log.info("--- Sequence setup ---") + def frame_velSetup(aux_sequence, frame, first_frame, vel_init=None): + #setup velocity + #with tf.device(resource_device): + vel_var_name = "velocity_f{:06d}".format(frame) + if first_frame and setup.training.velocity.pre_optimization and setup.training.velocity.pre_opt.first.grow.intervals: + vel_var_name = "velocity_f{:06d}_g000".format(frame) + elif setup.training.velocity.grow.intervals: + vel_var_name = "velocity_f{:06d}_g000".format(frame) + + vel_shape = (pre_opt_first_start_vel_shape if first_frame else pre_opt_start_vel_shape) if setup.training.velocity.pre_optimization else main_opt_start_vel_shape + + + if setup.training.velocity.decoder.active: + log.debug("using NN for velocity of frame %d", frame) + velocity = NeuralVelocityGrid(volume_decoder=velocity_decoder_model, boundary=vel_bounds, scale_renderer=scale_renderer, warp_renderer=warp_renderer, \ + device=resource_device, var_name=vel_var_name, parent_state=None, velocity_format=setup.training.velocity.decoder.velocity_format, \ + step_input_density=setup.training.velocity.decoder.step_input_density, \ + step_input_density_target=setup.training.velocity.decoder.step_input_density_target, \ + step_input_density_proxy=setup.training.velocity.decoder.step_input_density_proxy, \ + step_input_features=setup.training.velocity.decoder.step_input_features, \ + type_input_features=setup.training.velocity.decoder.type_input_features, \ + warp_input_indices=setup.training.velocity.decoder.warp_input_indices, \ + downscale_input_modes=setup.training.velocity.decoder.downscale_input_modes) + velocity.use_raw_images = setup.training.velocity.decoder.input_type=='RAW' + velocity.set_input_encoder(vel_input_encoder_model) + velocity.set_downscale_encoder(vel_downscale_encoder_model) + elif ("velocity" in aux_sequence[frame]) and (aux_sequence[frame].velocity is not None): + log.warning("Using dummy velocity for frame %d. TODO", frame) + velocity = VelocityGrid(main_opt_start_vel_shape, setup.data.velocity.init_std * setup.data.step, boundary=vel_bounds, scale_renderer=scale_renderer, warp_renderer=warp_renderer, device=resource_device, var_name='vel_dummy_f{:06d}'.format(frame)) + else: + if not setup.data.velocity.initial_value.upper().startswith('RAND'): + if first_frame or not setup.training.velocity.pre_optimization: + velocity = load_velocity(setup.data.velocity.initial_value, {'sim':setup.data.simulation,'frame':frame}, boundary=vel_bounds, scale_renderer=scale_renderer, warp_renderer=warp_renderer, device=resource_device, var_name=vel_var_name) + rel_vel_scale = float(setup.data.step/setup.data.velocity.load_step) + if rel_vel_scale!=1.: + log.debug("scale loaded velocity with %f", rel_vel_scale) + velocity.scale_magnitude(rel_vel_scale) + if velocity.centered_shape != vel_shape: + log.error("Shape %s of loaded velocity does not match required shape %s.", velocity.centered_shape, \ + (pre_opt_start_vel_shape if setup.training.velocity.pre_optimization else main_opt_start_vel_shape)) + sys.exit(1) + else: #not first and pre-opt. will be overwritten, put dummy data as file might not exist + velocity = VelocityGrid(main_opt_start_vel_shape, setup.data.velocity.init_std * setup.data.step, boundary=vel_bounds, scale_renderer=scale_renderer, warp_renderer=warp_renderer, device=resource_device, var_name='vel_dummy_f{:06d}'.format(frame)) + else: + velocity = VelocityGrid(vel_shape, setup.data.velocity.init_std * setup.data.step, boundary=vel_bounds, scale_renderer=scale_renderer, warp_renderer=warp_renderer, device=resource_device, var_name=vel_var_name) + if vel_init is not None: + velocity.assign(vel_init.x, vel_init.y, vel_init.z) + + if setup.data.velocity.init_mask != 'NONE': + if setup.data.velocity.init_mask == 'HULL': + vel_mask = aux_sequence[frame].hull + if setup.data.velocity.init_mask == 'HULL_NEXT': + frame_next = frame+setup.data.step + if frame_next in aux_sequence: + vel_mask = aux_sequence[frame_next].hull + else: + vel_mask = aux_sequence[frame].hull + elif setup.data.velocity.init_mask == 'HULL_TIGHT': + vel_mask = aux_sequence[frame].hull_tight + elif setup.data.velocity.init_mask == 'HULL_TIGHT_NEXT': + frame_next = frame+setup.data.step + if frame_next in aux_sequence: + vel_mask = aux_sequence[frame_next].hull_tight + else: + vel_mask = aux_sequence[frame].hull_tight + else: + raise ValueError("Unknown velocity mask %s"%setup.data.velocity.init_mask) + hull_x = scale_renderer.resample_grid3D_aligned(vel_mask, velocity.x_shape, align_x='STAGGER_OUTPUT') + hull_y = scale_renderer.resample_grid3D_aligned(vel_mask, velocity.y_shape, align_y='STAGGER_OUTPUT') + hull_z = scale_renderer.resample_grid3D_aligned(vel_mask, velocity.z_shape, align_z='STAGGER_OUTPUT') + velocity.assign(x=velocity.x*hull_x, y=velocity.y*hull_y, z=velocity.z*hull_z) + return velocity + + def frame_velTargetSetup(aux_sequence, frame): + if not ("velocity" in aux_sequence[frame]) and (aux_sequence[frame].velocity is not None): + raise ValueError("") + vel_var_name = "velocityTarget_f{:06d}".format(frame) + velocity = VelocityGrid.from_staggered_combined(aux_sequence[frame].velocity, as_var=False, boundary=vel_bounds, \ + scale_renderer=scale_renderer, warp_renderer=warp_renderer, device=resource_device, var_name=vel_var_name, trainable=False) + return velocity + + def frame_densSetup(aux_sequence, frame, first_frame): + inflow_init = None + inflow_mask = None + inflow_offset = None + if setup.data.density.inflow.active: + base_inflow_mask, base_inflow_shape, base_inflow_offset = create_inflow(tf.squeeze(aux_sequence[frame].hull, (0,-1)), setup.data.density.inflow.hull_height, setup.data.density.inflow.height) + if base_inflow_mask is not None: #setup.data.density.inflow.active: + base_inflow_mask = tf.reshape(base_inflow_mask, [1]+base_inflow_shape+[1]) + log.info("Base Inflow: %s at %s", base_inflow_shape, base_inflow_offset) + inflow_init = 'CONST' + inflow_offset = current_grow_shape(base_inflow_offset, 0, setup.training.density.grow.factor, setup.training.density.grow.intervals) + inflow_shape = current_grow_shape(base_inflow_shape, 0, setup.training.density.grow.factor, setup.training.density.grow.intervals, cast_fn=lambda x: max(round(x),1)) + inflow_mask = scale_renderer.resample_grid3D_aligned(base_inflow_mask, inflow_shape) + else: + log.error("Failed to build inflow.") + + if setup.training.density.decoder.active: + log.debug("using NN for density of frame %d", frame) + density = NeuralDensityGrid(volume_decoder=density_decoder_model, scale_renderer=scale_renderer, parent_state=None, \ + base_input=setup.training.density.decoder.base_input, \ + step_input_density=setup.training.density.decoder.step_input_density, \ + step_input_density_target=setup.training.density.decoder.step_input_density_target, \ + step_input_features=setup.training.density.decoder.step_input_features, \ + type_input_features=setup.training.density.decoder.type_input_features, \ + device=resource_device, is_SDF=setup.data.SDF, base_SDF_mode=setup.training.density.decoder.base_SDF_mode) + density.use_raw_images = setup.training.density.decoder.input_type=='RAW' + elif ("density" in aux_sequence[frame]) and (aux_sequence[frame].density is not None): + dens_hull = None + density_grid_shape = GridShape.from_tensor(aux_sequence[frame].density).spatial_vector.as_shape + density = DensityGrid(density_grid_shape, d=aux_sequence[frame].density, as_var=False, \ + scale_renderer=scale_renderer, hull=dens_hull, inflow=inflow_init, inflow_offset=inflow_offset, inflow_mask=inflow_mask, \ + device=resource_device, restrict_to_hull=setup.training.density.use_hull, is_SDF=setup.data.SDF) + log.info("Initialized density of frame %d for random loaded data with shape %s", frame, density_grid_shape) + # global curr_dens_shape + # curr_dens_shape = density_grid_shape + else: + #setup density with start scale + dens_hull = scale_renderer.resample_grid3D_aligned(aux_sequence[frame].hull, curr_dens_shape) # if setup.training.density.use_hull else None + #with tf.device(resource_device): + dens_var_name = "density_f{:06d}".format(frame) + if setup.data.density.initial_value.upper()=="CONST": + density = DensityGrid(curr_dens_shape, setup.data.density.scale, scale_renderer=scale_renderer, hull=dens_hull, inflow=inflow_init, inflow_offset=inflow_offset, inflow_mask=inflow_mask, \ + device=resource_device, var_name=dens_var_name, restrict_to_hull=setup.training.density.use_hull, is_SDF=setup.data.SDF) + log.debug("Initialized density with constant value") + elif setup.data.density.initial_value.upper()=="ESTIMATE": + density_estimate = renderer.unproject(sim_transform, aux_sequence[frame].targets.images, target_cameras)#*setup.data.density.scale + density = DensityGrid(curr_dens_shape, d=density_estimate, scale_renderer=scale_renderer, hull=dens_hull, inflow=inflow_init, inflow_offset=inflow_offset, inflow_mask=inflow_mask, \ + device=resource_device, var_name=dens_var_name, restrict_to_hull=setup.training.density.use_hull, is_SDF=setup.data.SDF) + log.debug("Initialized density with estimate from targets") + elif setup.data.density.initial_value.upper()=="HULL": + density = DensityGrid(curr_dens_shape, d=dens_hull*setup.data.density.scale, scale_renderer=scale_renderer, hull=dens_hull, inflow=inflow_init, inflow_offset=inflow_offset, inflow_mask=inflow_mask, \ + device=resource_device, var_name=dens_var_name, restrict_to_hull=setup.training.density.use_hull, is_SDF=setup.data.SDF) + log.debug("Initialized density with visual hull from targets") + elif setup.data.density.initial_value.upper()=="HULL_TIGHT": + h = scale_renderer.resample_grid3D_aligned(aux_sequence[frame].hull_tight, curr_dens_shape) + density = DensityGrid(curr_dens_shape, d=h*setup.data.density.scale, scale_renderer=scale_renderer, hull=dens_hull, inflow=inflow_init, inflow_offset=inflow_offset, inflow_mask=inflow_mask, \ + device=resource_device, var_name=dens_var_name, restrict_to_hull=setup.training.density.use_hull, is_SDF=setup.data.SDF) + del h + log.debug("Initialized density with visual hull from targets") + else: #load + if first_frame or not setup.training.density.pre_optimization: + try: + path = run_index[setup.data.density.initial_value] + if path is None: + path = setup.data.density.initial_value + path = path.format(sim=setup.data.simulation, frame=frame) + # TODO: remove hull and inflow params to load original. they may be non-existent or incompatible from older saves + density = DensityGrid.from_file(path, scale_renderer=scale_renderer, device=resource_device, var_name=dens_var_name, restrict_to_hull=setup.training.density.use_hull, is_SDF=setup.data.SDF) #, hull=dens_hull, inflow=inflow_mask, inflow_offset=inflow_offset + if density.hull is None: #no hull loaded, but a hull should be used, so use the newly build one + density.hull = dens_hull + except: + log.exception("Falied to load density for frame %d from '%s'", frame, path) + sys.exit(1) + else: + log.debug("Initialized density for frame %d with value loaded from %s", frame, setup.data.density.initial_value) + if density.shape != curr_dens_shape: + log.error("Shape %s of density loaded from '%s' does not match required shape %s.", velocity.centered_shape, path, curr_dens_shape) + sys.exit(1) + else: #not first and pre-opt. will be overwritten, put dummy data as file might not exist + density = DensityGrid(curr_dens_shape, setup.data.density.scale, scale_renderer=scale_renderer, hull=dens_hull, inflow=inflow_init, inflow_offset=inflow_offset, inflow_mask=inflow_mask, \ + device=resource_device, var_name=dens_var_name, restrict_to_hull=setup.training.density.use_hull, is_SDF=setup.data.SDF) + + with tf.device(resource_device): + density.base_hull = tf.identity(aux_sequence[frame].hull) #if setup.training.density.use_hull else None + if inflow_mask is not None: #setup.data.density.inflow.active: + density.base_inflow_mask = tf.identity(base_inflow_mask) + density.base_inflow_shape = base_inflow_shape + density.base_inflow_offset = base_inflow_offset + return density + + def frame_densTargetSetup(aux_sequence, frame): + if not ("density" in aux_sequence[frame]) and (aux_sequence[frame].density is not None): + raise ValueError("") + density_grid_shape = GridShape.from_tensor(aux_sequence[frame].density).spatial_vector.as_shape + density = DensityGrid(density_grid_shape, d=aux_sequence[frame].density, as_var=False, \ + scale_renderer=scale_renderer, hull=None, inflow=None, inflow_offset=None, inflow_mask=None, \ + device=resource_device, restrict_to_hull=setup.training.density.use_hull, is_SDF=setup.data.SDF) + return density + + def frame_densProxySetup(aux_sequence, frame): + if not setup.training.density.decoder.active: + raise RuntimeError("no density network available for density proxy") + density = NeuralDensityGrid(volume_decoder=density_decoder_model, scale_renderer=scale_renderer, parent_state=None, \ + base_input=setup.training.density.decoder.base_input, \ + step_input_density=setup.training.density.decoder.step_input_density, \ + step_input_density_target=setup.training.density.decoder.step_input_density_target, \ + step_input_features=setup.training.density.decoder.step_input_features, \ + type_input_features=setup.training.density.decoder.type_input_features, \ + device=resource_device, is_SDF=setup.data.SDF, base_SDF_mode=setup.training.density.decoder.base_SDF_mode) + density.use_raw_images = setup.training.density.decoder.input_type=='RAW' + return density + + def frame_setup(aux_sequence, frame, first_frame, prev=None, vel_init=None): + log.info("--- Setup frame %d ---", frame) + velocity = frame_velSetup(aux_sequence, frame, first_frame, vel_init) + density = frame_densSetup(aux_sequence, frame, first_frame) + + if setup.training.density.decoder.active or setup.training.velocity.decoder.active: + state = NeuralState(density, velocity, target_encoder=target_encoder_model, encoder_output_types=setup.training.view_encoder.encoder, \ + target_lifting=setup.training.view_encoder.lifting, lifting_renderer=lifting_renderer, target_merging=setup.training.view_encoder.merge, \ + volume_encoder=volume_encoder_model, frame=frame, prev=prev, transform=sim_transform.copy_no_data(), lifting_network=lifting_network_model, frame_merge_network=frame_merge_network_model)# + density.parent_state = state + velocity.parent_state = state + + if setup.training.density.decoder.active: + state.density_proxy = frame_densProxySetup(aux_sequence, frame) + state.density_proxy.parent_state = state + + if setup.training.velocity.decoder.active and setup.training.velocity.decoder.recursive_MS: + num_levels = get_max_recursive_MS_grow_levels(setup.training.velocity.decoder) + scale_factor = setup.training.velocity.decoder.recursive_MS_scale_factor + velocity.set_recursive_MS(num_levels, scale_factor, shared_decoder=setup.training.velocity.decoder.recursive_MS_shared_model, \ + train_mode=setup.training.velocity.decoder.recursive_MS_train_mode, direct_input=setup.training.velocity.decoder.recursive_MS_direct_input, \ + max_level_input=setup.training.velocity.decoder.recursive_MS_use_max_level_input) + log.info("Set recursive-MS velocity for frame %d with %d levels.", frame, num_levels) + + if setup.training.density.decoder.active and setup.training.density.decoder.recursive_MS: + num_levels = get_max_recursive_MS_grow_levels(setup.training.density.decoder) + scale_factor = setup.training.density.decoder.recursive_MS_scale_factor + density.set_recursive_MS(num_levels, scale_factor, shared_decoder=setup.training.density.decoder.recursive_MS_shared_model, train_mode=setup.training.density.decoder.recursive_MS_train_mode, as_residual=setup.training.density.decoder.recursive_MS_residual, direct_input=setup.training.density.decoder.recursive_MS_direct_input) + if state.has_density_proxy: + state.density_proxy.set_recursive_MS(num_levels, scale_factor, shared_decoder=setup.training.density.decoder.recursive_MS_shared_model, train_mode=setup.training.density.decoder.recursive_MS_train_mode, as_residual=setup.training.density.decoder.recursive_MS_residual, direct_input=setup.training.density.decoder.recursive_MS_direct_input) + log.info("Set recursive-MS density for frame %d with %d levels.", frame, num_levels) + else: + state = State(density, velocity, frame=frame, prev=prev, transform=sim_transform.copy_no_data()) + with tf.device(resource_device): + state.hull = tf.identity(aux_sequence[frame].hull_tight) + state.data_path = os.path.join(setup.paths.data, 'frame_{:06d}'.format(frame)) + os.makedirs(state.data_path, exist_ok=True) + #setup targets + + if ("velocity" in aux_sequence[frame]) and (aux_sequence[frame].velocity is not None): + state.velocity_target = frame_velTargetSetup(aux_sequence, frame) + if ("density" in aux_sequence[frame]) and (aux_sequence[frame].density is not None): + state.density_target = frame_densTargetSetup(aux_sequence, frame) + + if setup.training.velocity.pre_optimization: raise NotImplementedError + grow_handler.start_iteration(0, is_pre_opt=setup.training.density.pre_optimization) + state_set_targets(state, aux_sequence) + return state + + def sequence_setup(aux_sequence): + sequence = [] + prev = None + vel_init = None + first_frame = True + for frame in frames: + state = frame_setup(aux_sequence, frame, first_frame, prev, vel_init) + curr_cam_res_MS = grow_handler.get_camera_MS_scale_shapes() + + + state.base_target_cameras = setup_target_cameras(target_cameras, curr_cam_res, jitter=setup.training.density.camera_jitter) + state.set_base_target_cameras_MS({scale: setup_target_cameras(target_cameras, shape, jitter=setup.training.density.camera_jitter) for scale, shape in curr_cam_res_MS.items()}) + + log.debug('Write target images') + state.base_targets_raw.save(renderer, state.data_path, "PNG") + state.base_targets_raw.save_MS_stack(renderer, state.data_path, "PNG") + if isinstance(state.base_targets_raw, ImageSetMS): + state.base_targets_raw.save_base_MS_stack(renderer, state.data_path, "PNG") + state.base_targets.save(renderer, state.data_path, "PNG") + state.base_bkgs.save(renderer, state.data_path, "PNG") + if state.has_masks: + state.base_masks.save(renderer, state.data_path, "PNG") + + # random velocity initialization, but same for each frame + if setup.data.velocity.initial_value.upper()=='RAND_CONST': + vel_init = state.velocity + prev = state + sequence.append(state) + first_frame = False + return sequence + + sequence = sequence_setup(val_sequence) #aux_sequence) + del sequence_setup + del frame_setup + del frame_densSetup + del frame_velSetup + del aux_sequence + + for i in range(len(sequence)-1): + sequence[i].next = sequence[i+1] + sequence = Sequence(sequence) + + with tf.device(resource_device): + if setup.training.optimize_buoyancy: + buoyancy = tf.Variable(initial_value=setup.data.initial_buoyancy, dtype=tf.float32, name='buoyancy', trainable=True) + else: + buoyancy = tf.constant(setup.data.initial_buoyancy, dtype=tf.float32) + + if setup.training.velocity.pre_opt.first.iterations>0 and setup.training.velocity.pre_optimization: #and setup.training.velocity.pre_optimization ? + curr_vel_shape = pre_opt_start_vel_shape + else: + curr_vel_shape = main_opt_start_vel_shape + + s = "Sequence setup:" + i=0 + for state in sequence: + p = -1 if state.prev is None else state.prev.frame + n = -1 if state.next is None else state.next.frame + s += "\n{:4d}: frame {:06d}: p={:06d}, n={:06d}".format(i, state.frame, p, n) + i +=1 + log.info(s) + + light_var_list = [] + if setup.training.light.optimize: + log.debug('Initialize variables (density %f and light intensity %f)', setup.data.density.scale, lights[0].i) + var_intensity = tf.get_variable(name='intensity', initializer=lights[0].i, constraint=lambda var: tf.clip_by_value(var, setup.training.light.min, setup.training.light.max), dtype=tf.float32, trainable=True) + lights[0].i=var_intensity + var_ambient_intensity = tf.get_variable(name='ambient_intensity', initializer=lights[1].i, constraint=lambda var: tf.clip_by_value(var, setup.training.light.min, setup.training.light.max), dtype=tf.float32, trainable=True) + lights[1].i=var_ambient_intensity + light_var_list = [var_intensity, var_ambient_intensity] + + disc_dump_samples = setup.debug.disc_dump_samples + if setup.training.discriminator.active: + log.info('Setup discriminator') + disc_real_data = None + disc_input_steps = None + if setup.training.discriminator.temporal_input.active: + disc_input_steps = list(range(*setup.training.discriminator.temporal_input.step_range)) + if 0 in disc_input_steps: + disc_input_steps.remove(0) + if make_disc_dataset: + log.debug('Setup discriminator training data.') + disc_real_res = tf.Variable(initial_value=disc_cam_resolution[1:], name='disc_real_res', dtype=tf.int32, trainable=False) + + if setup.training.discriminator.temporal_input.active: raise NotImplementedError + if setup.training.discriminator.conditional_hull: raise NotImplementedError + + class DiscDataMapper: + def __init__(self, dataset, res_var=None): + self.__dataset = dataset + self.__res_var = res_var + def get_next(self): + batch = self.__dataset.frame_targets(0)[0] + shape = shape_list(batch) + #log.info("Raw batch shape %s, required NHWC.", shape) + samples = tf.unstack(batch, axis=0) + batch = tf.stack([sample[np.random.choice(shape[1])] for sample in samples], axis=0) + if self.__res_var is not None: + batch = wrap_resize_images(batch, self.__res_var.numpy()) + #log.info("Processed batch shape %s, required NHWC.", shape_list(batch)) + self.__dataset.step() + return batch + #if setup.data.SDF: raise NotImplementedError("Discriminator not supported in SDF mode.") + + + disc_real_data = DiscDataMapper(disc_dataset, disc_real_res if setup.data.discriminator.scale_real_to_cam else None) + + disc_in_channel = color_channel + if setup.training.discriminator.conditional_hull: + disc_in_channel += 1 + if setup.training.discriminator.temporal_input.active: + disc_in_channel *= 3 + if setup.data.discriminator.crop_size=="CAMERA": + disc_in_shape = disc_cam_resolution[1:] + [disc_in_channel] + else: + disc_in_shape = list(setup.data.discriminator.crop_size) + [disc_in_channel] #disc_targets_shape[-3:] + if train_disc and setup.training.discriminator.history.samples>0: + log.debug("Initializing fake samples history buffer for discriminator experience replay.") + if setup.training.discriminator.history.load is not None: + history_path = run_index[setup.training.discriminator.history.load] + if history_path is None: + history_path = setup.training.discriminator.history.load + raise NotImplementedError() + log.debug('Setup discriminator model') + + # compatibility + if setup.training.discriminator.use_fc==True: + setup.training.discriminator.use_fc=[] + elif setup.training.discriminator.use_fc==False: + setup.training.discriminator.use_fc=None + + if setup.training.discriminator.model is not None: + model_path = run_index[setup.training.discriminator.model] + if model_path is None: + model_path = setup.training.discriminator.model + disc_model=tf.keras.models.load_model(model_path, custom_objects=custom_keras_objects) + log.info('Restored model from %s', model_path) + else: + disc_model=discriminator(disc_in_shape, layers=setup.training.discriminator.layers, strides=setup.training.discriminator.stride, kernel_size=setup.training.discriminator.kernel_size, final_fc=setup.training.discriminator.use_fc, activation=setup.training.discriminator.activation, alpha=setup.training.discriminator.activation_alpha, noise_std=setup.training.discriminator.noise_std, padding=setup.training.discriminator.padding) + log.debug('Built discriminator keras model') + #disc_model(disc_targets, training=False) + disc_weights = disc_model.get_weights() + disc_model.summary(print_fn= lambda s: log.info(s)) + + if np.any(np.less(disc_in_shape[:-1], disc_cam_resolution[1:])) and setup.training.discriminator.use_fc is not None and (not setup.data.discriminator.scale_input_to_crop): + log.error("Base fake sample camera resolution exeeds rigid discriminator input resolution. Use the patch discriminator or enable input scaling.") + curr_disc_cam_res = current_grow_shape(disc_cam_resolution, 0, setup.training.density.grow.factor, setup.training.density.grow.intervals) + for camera in disc_cameras: + camera.transform.grid_size = curr_disc_cam_res + if setup.training.discriminator.fake_camera_jitter: + camera.jitter = camera.depth_step + disc_real_res.assign(curr_disc_cam_res[1:]) + #END if setup.training.discriminator.active + + def grow_networks(sequence, iteration, preopt_iterations_density=0, preopt_iterations_velocity=0): + for state in sequence: + if isinstance(state.density, NeuralDensityGrid) and isinstance(state.density.volume_decoder, GrowingUNet): + state.density.volume_decoder.step(iteration + preopt_iterations_density) + + if isinstance(state.velocity, NeuralVelocityGrid) and isinstance(state.velocity.volume_decoder, GrowingUNet): + state.velocity.volume_decoder.step(iteration + preopt_iterations_velocity) + + def grow_density_networks(sequence, iteration, preopt_iterations_density=0, preopt_iterations_targets=0): + for state in sequence: + if isinstance(state.density, NeuralDensityGrid) and isinstance(state.density.volume_decoder, GrowingUNet): + state.density.volume_decoder.step(iteration + preopt_iterations_density) + + def grow_velocity_networks(sequence, iteration, preopt_iterations_velocity=0, preopt_iterations_targets=0): + for state in sequence: + if isinstance(state.velocity, NeuralVelocityGrid) and isinstance(state.velocity.volume_decoder, GrowingUNet): + state.velocity.volume_decoder.step(iteration + preopt_iterations_velocity) + + def set_density_network_level(sequence, grid_size=None): + for state in sequence: + if isinstance(state.density, NeuralDensityGrid) and isinstance(state.density.volume_decoder, GrowingUNet): + net = state.density.volume_decoder + gs = grid_size if grid_size is not None else state.transform.grid_size + level = min(net.num_levels, GrowingUNet.get_max_levels(gs, scale_factor=net.level_scale_factor, min_size=3)) - 1 + net.set_active_level(level) + log.debug("Set density generator level to %d for grid size %s (frame %d)", level, gs, state.frame) + + def set_velocity_network_level(sequence, grid_size=None): + for state in sequence: + if isinstance(state.velocity, NeuralVelocityGrid) and isinstance(state.velocity.volume_decoder, GrowingUNet): + net = state.velocity.volume_decoder + level = min(net.num_levels, GrowingUNet.get_max_levels(grid_size if grid_size is not None else state.transform.grid_size, scale_factor=net.level_scale_factor, min_size=3)) - 1 + net.set_active_level(level) + log.debug("Set velocity generator level to %d (frame %d)", level, state.frame) + + def set_growing_network_level(model, grid_size, min_size): + assert isinstance(model, GrowingUNet) + level = min(model.num_levels, GrowingUNet.get_max_levels(grid_size, scale_factor=model.level_scale_factor, min_size=min_size, allow_padded=True)) - 1 + if not level==model.get_active_level(): + log.info("Set level of %s from %d to %d for grid size %s.", model.name, model.get_active_level(), level, grid_size) + model.set_active_level(level) + + def scale_state_networks(sequence): + # set recursive MS scales for active state networks + # TODO: use some density scale settings for now + cam_grid_size = grow_handler.get_camera_shape()[1:] + grid_size = grow_handler.get_density_shape() #state.transform.grid_size + for state in sequence: + if isinstance(state.target_encoder, GrowingUNet): + min_size = setup.training.view_encoder.min_grid_res #min(grow_handler.get_image_MS_scale_shapes()[0]) + set_growing_network_level(model=state.target_encoder, grid_size=cam_grid_size, min_size=min_size) + + if isinstance(state.volume_encoder, GrowingUNet): + min_size = setup.training.volume_encoder.min_grid_res + set_growing_network_level(model=state.volume_encoder, grid_size=grid_size, min_size=min_size) + if isinstance(state.lifting_network, GrowingUNet): + min_size = setup.training.lifting_network.min_grid_res + state.lifting_network.set_active_level_from_grid_size(grid_size=cam_grid_size, min_size=min_size, lifting_size=grid_size) + if isinstance(state.frame_merge_network, GrowingUNet): + min_size = setup.training.frame_merge_network.min_grid_res + set_growing_network_level(model=state.frame_merge_network, grid_size=grid_size, min_size=min_size) + + def scale_density(sequence, iteration, factor, intervals, base_shape, actions=[], save=True, scale_all=False, verbose=True): + global curr_dens_shape, curr_cam_res + dens_shape = grow_handler.get_density_shape() + def check_scale_density(state): + recursive_MS_grow_level = grow_handler.get_density_MS_scale() + return state.density.shape!=dens_shape \ + or (state.has_density_neural and state.density.recursive_MS and state.density.recursive_MS_current_level!=recursive_MS_grow_level) \ + or (state.has_density_proxy and state.density_proxy.recursive_MS and state.density_proxy.recursive_MS_current_level!=recursive_MS_grow_level) + scale_sequence = sequence if scale_all else [state for state in sequence if check_scale_density(state)] #.density.shape!=dens_shape] + if scale_sequence: + curr_cam_res = grow_handler.get_camera_shape() + curr_cam_res_MS = grow_handler.get_camera_MS_scale_shapes() + curr_tar_res_MS = grow_handler.get_image_MS_scale_shapes() + (log.info if verbose else log.debug)("Rescaling density of frames %s from %s to %s in iteration %d; cameras to %s", [_.frame for _ in scale_sequence], \ + [_.density.shape for _ in scale_sequence], dens_shape, iteration, curr_cam_res) + log.debug("Saving sequence") + with profiler.sample("Save sequence"): + try: + for state in scale_sequence: + if save and type(state.density)==DensityGrid: #not isinstance(state.density, (NeuralDensityGrid, WarpedDensityGrid)): + state.density.save(os.path.join(state.data_path, \ + "density_{}-{}-{}_{}.npz".format(*state.density.shape, iteration))) + except: + log.warning("Failed to save density before scaling from %s to %s in iteration %d:", \ + curr_dens_shape, dens_shape, iteration, exc_info=True) + log.debug("Rescaling density sequence") + with profiler.sample('Rescale densities'): + for state in scale_sequence: + #scale hull and inflow to new shape based on base values + hull = state.density.scale_renderer.resample_grid3D_aligned(state.density.base_hull, dens_shape)if state.density.hull is not None else None + + if type(state.density)==DensityGrid and state.density.shape!=dens_shape: #not isinstance(state.density, NeuralDensityGrid): + d = state.density.scaled(dens_shape) + if state.density._inflow is not None: + raise NotImplementedError("TODO: randomized growing for inflow shape") + if_off = current_grow_shape(state.density.base_inflow_offset, iteration, factor, intervals) + if_shape = current_grow_shape(state.density.base_inflow_shape, iteration, factor, intervals, cast_fn=lambda x: max(round(x),1)) #cast_fn=math.ceil + if_scaled = upscale_renderer.resample_grid3D_aligned(state.density._inflow, if_shape) + if_mask = None if state.density.inflow_mask is None else state.density.scale_renderer.resample_grid3D_aligned(state.density.base_inflow_mask, if_shape) + log.info("Frame %04d: inflow to %s, offset to %s", state.frame, if_shape, if_off) + density = DensityGrid(shape=dens_shape, d=d, as_var=state.density.is_var, hull=hull, inflow=if_scaled, inflow_offset=if_off, inflow_mask=if_mask, \ + scale_renderer=state.density.scale_renderer, device=state.density._device, var_name=state.density._name+"_scaled", restrict_to_hull=state.density.restrict_to_hull, is_SDF=state.density.is_SDF) + else: + density = DensityGrid(shape=dens_shape, d=d, as_var=state.density.is_var, hull=hull, \ + scale_renderer=state.density.scale_renderer, device=state.density._device, var_name=state.density._name+"_scaled", restrict_to_hull=state.density.restrict_to_hull, is_SDF=state.density.is_SDF) + if hull is not None: + density.base_hull = state._density.base_hull + if density._inflow is not None: + density.base_inflow_mask = state._density.base_inflow_mask + density.base_inflow_shape = state._density.base_inflow_shape + density.base_inflow_offset = state._density.base_inflow_offset + state.density = density + + if state.has_density_target: + state.density_target.rescale(dens_shape, None) + + state.transform.grid_size = dens_shape + + if type(state.density)==NeuralDensityGrid and state.density.recursive_MS: + copied_weights = False + recursive_MS_grow_level = grow_handler.get_density_MS_scale() + # for rand: get_grow_level(vel_scale) + if state.density.recursive_MS_current_level!=recursive_MS_grow_level: + copy_weights=((not setup.training.density.decoder.recursive_MS_shared_model) \ + and setup.training.density.decoder.recursive_MS_copy_on_grow \ + and (state.density.recursive_MS_current_level < recursive_MS_grow_level) \ + and not copied_weights) + if copy_weights: + save_NNmodel(velocity_decoder_model, 'velocity_decoder_grow%02d'%(state.density.recursive_MS_current_level, ), setup.paths.data) + state.density.set_recursive_MS_level(recursive_MS_grow_level, \ + copy_weights=copy_weights) #and state is sequence[0] + log.debug("Set recursive MS density level of frame %d to %d%s", state.frame, recursive_MS_grow_level,", copy weights from previous level." if copy_weights else ".") + copied_weights = copy_weights or copied_weights + + if state.has_density_proxy and state.density_proxy.recursive_MS: + recursive_MS_grow_level = grow_handler.get_density_MS_scale() + if state.density_proxy.recursive_MS_current_level!=recursive_MS_grow_level: + if setup.training.density.decoder.recursive_MS_copy_on_grow: raise NotImplementedError + state.density_proxy.set_recursive_MS_level(recursive_MS_grow_level) + + AABB_corners_WS = dens_transform.transform_AABB(*hull_AABB_OS(tf.squeeze(state.density.hull, (0,-1))), True) if setup.rendering.target_cameras.crop_frustum else None + state.base_target_cameras = setup_target_cameras(target_cameras, curr_cam_res, AABB_corners_WS,setup.rendering.target_cameras.crop_frustum_pad, jitter=setup.training.density.camera_jitter) + target_cameras_MS = {scale: setup_target_cameras(target_cameras, shape, AABB_corners_WS, setup.rendering.target_cameras.crop_frustum_pad, jitter=setup.training.density.camera_jitter) for scale, shape in curr_cam_res_MS.items()} + state.set_base_target_cameras_MS(target_cameras_MS) + curr_dens_shape = dens_shape + #target and cams scale + log.debug("Rescaling cameras") + if setup.training.discriminator.active: + global curr_disc_cam_res + if grow_handler.is_randomize_shape or grow_handler.is_iterate_shapes: + raise NotImplementedError("TODO: randomized growing for disc cam shape") + curr_disc_cam_res = current_grow_shape(disc_cam_resolution, iteration, factor, intervals) + log.info("Scaling discriminator camera resolution to %s", curr_disc_cam_res) + for camera in disc_cameras: + camera.transform.grid_size = curr_disc_cam_res + if setup.training.discriminator.fake_camera_jitter: + camera.jitter = camera.depth_step + disc_real_res.assign(curr_disc_cam_res[1:]) + if train_disc and setup.training.discriminator.history.samples>0: + if setup.training.discriminator.history.reset_on_density_grow: + log.info("Reset disc history after rescale") + disc_ctx.history.reset() + # targets scaled from base + if not setup.data.randomize>0: #randomized data is loaded after calling scale_density (so no reason to resize here) + raise NotImplementedError + log.debug("Rescaling targets from base") + with profiler.sample('Rescale targets'): + for state in scale_sequence: + + state.base_targets_raw.resize(curr_cam_res[1:]) + state.base_targets.resize(curr_cam_res[1:]) + state.base_bkgs.resize(curr_cam_res[1:]) + if state.has_masks: + state.base_masks.resize(curr_cam_res[1:]) + + state.base_targets_raw.create_MS_stack(curr_tar_res_MS) + state.base_targets.create_MS_stack(curr_tar_res_MS) + state.base_bkgs.create_MS_stack(curr_tar_res_MS) + if state.has_masks: + state.base_masks.create_MS_stack(curr_tar_res_MS) + return True + else: + return False + #END if rescale density + + def randomize_scale(sequence, *, min_size_abs, min_size_rel, max_shape, train_cam_res, disc_cam_res=None): + # choose random grid size between min and max (weighted?) + # set transform of states to this grid size + # set network levels + assert setup.data.randomize>0 + assert all(type(state.density)==NeuralDensityGrid for state in sequence) + + def lerp_shape(a,b,t): + return [int(round(_)) for _ in lerp_vector(a, b, t)] + + if min_size_rel==1: + rand_shape = max_shape + min_shape = max_shape + t = 1 + elif min_size_rel<0: + max_size = min(max_shape) + dim_factors = [_/max_size for _ in max_shape] + min_shape = [int(_*min_size_abs) for _ in dim_factors] + net_factor = 2 #TODO + sizes = [min_size_abs] + while sizes[-1]*net_factor <= max_size: + sizes.append(sizes[-1]*net_factor) + rand_size = np.random.choice(sizes) + rand_shape = [int(_*rand_size) for _ in dim_factors] + t = np.mean(rand_shape) / np.mean(max_shape) + else: + max_size = min(max_shape) + #clamp + min_size_abs = max(min(max_size, min_size_abs), 1) + min_size_rel = min(min_size_rel, 1.0) + + min_size = max(min_size_abs, int(max_size*min_size_rel)) + min_factor = min_size/max_size + min_shape = [int(np.ceil(_*min_factor)) for _ in max_shape] + + t = np.random.random() + rand_shape = lerp_shape(min_shape, max_shape, t) + t = np.mean(rand_shape) / np.mean(max_shape) + rand_train_cam_res = lerp_shape([0,0,0], train_cam_res, t) + + if min_size_rel==1: + log.debug("set max resolution: grid=%s, target=%s", rand_shape, rand_train_cam_res) + else: + log.debug("randomize resolution: grid=%s (%s - %s), target=%s (%s)", rand_shape, min_shape, max_shape, rand_train_cam_res, train_cam_res) + + for state in sequence: + assert state.density.hull is None + state.transform.grid_size = rand_shape + if setup.rendering.target_cameras.crop_frustum: raise NotImplementedError + state.base_target_cameras = setup_target_cameras(target_cameras, rand_train_cam_res, None, setup.rendering.target_cameras.crop_frustum_pad, jitter=setup.training.density.camera_jitter) + state.base_targets_raw.resize(rand_train_cam_res[1:]) + state.base_targets.resize(rand_train_cam_res[1:]) + state.base_bkgs.resize(rand_train_cam_res[1:]) + if state.has_masks: + state.base_masks.resize(rand_train_cam_res[1:]) + + set_density_network_level(sequence) + set_velocity_network_level(sequence) + + if setup.training.discriminator.active: + rand_disc_cam_res = lerp_shape([0,0,0], disc_cam_res, t) + for camera in disc_cameras: + camera.transform.grid_size = rand_disc_cam_res + if setup.training.discriminator.fake_camera_jitter: + camera.jitter = camera.depth_step + disc_real_res.assign(rand_disc_cam_res[1:]) + + def scale_velocity(sequence, iteration, factor, scale_magnitude, intervals, base_shape, verbose=True): + global curr_vel_shape, z, curr_cam_res + #vel_shape = current_grow_shape(base_shape, iteration, factor, intervals) + vel_shape = grow_handler.get_velocity_shape() + # for rand: rand_vel_shape(min_shape, vel_shape) + scale_sequence = [] + for state in sequence: + if state.velocity.centered_shape!=vel_shape: + scale_sequence.append(state) + if scale_sequence: + #curr_cam_res = current_grow_shape(train_cam_resolution, iteration, factor, intervals) + curr_cam_res = grow_handler.get_camera_shape() + (log.info if verbose else log.debug)("Rescaling velocity of frames %s from %s to %s in iteration %d, magnitude: %s; cameras to %s", [_.frame for _ in scale_sequence], \ + [_.velocity.centered_shape for _ in scale_sequence], vel_shape, iteration, scale_magnitude, curr_cam_res) + log.debug("Saving sequence") + with profiler.sample("Save sequence"): + try: + for state in scale_sequence: + if type(state.velocity)==VelocityGrid: + state.velocity.save(os.path.join(state.data_path, \ + "velocity_{}-{}-{}_{}.npz".format(*state.velocity.centered_shape, iteration))) + except: + log.warning("Failed to save velocity before scaling from %s to %s in iteration %d:", \ + state.velocity.centered_shape, vel_shape, iteration, exc_info=True) + log.debug("Rescaling velocity sequence") + with profiler.sample('Rescale velocities'): + copied_weights = False + for state in scale_sequence: + log.debug("Rescaling velocity frame %d", state.frame) + state.velocity.set_centered_shape(vel_shape) #also affects NeuralDensityGrid grid size + if type(state.velocity)==VelocityGrid: + state.rescale_velocity(vel_shape, scale_magnitude=scale_magnitude, device=resource_device) + if type(state.velocity)==NeuralVelocityGrid and state.velocity.recursive_MS: + recursive_MS_grow_level = grow_handler.get_velocity_MS_scale() + log.debug("NVG target level: %d, from %d", recursive_MS_grow_level, state.velocity.recursive_MS_current_level) + if state.velocity.recursive_MS_current_level!=recursive_MS_grow_level: + copy_weights=((not setup.training.velocity.decoder.recursive_MS_shared_model) \ + and setup.training.velocity.decoder.recursive_MS_copy_on_grow \ + and (state.velocity.recursive_MS_current_level < recursive_MS_grow_level) \ + and not copied_weights) + if copy_weights: + save_NNmodel(velocity_decoder_model, 'velocity_decoder_grow%02d'%(state.velocity.recursive_MS_current_level, ), setup.paths.data) + state.velocity.set_recursive_MS_level(recursive_MS_grow_level, \ + copy_weights=copy_weights) #and state is sequence[0] + log.info("Set recursive MS velocity level of frame %d to %d%s", state.frame, recursive_MS_grow_level,", copy weights from previous level." if copy_weights else ".") + copied_weights = copy_weights or copied_weights + curr_vel_shape = vel_shape + + if not setup.data.randomize>0: #randomized data is loaded after calling scale_density + raise NotImplementedError + log.debug("Rescaling targets from base") + with profiler.sample('Rescale targets'): + for state in scale_sequence: + state.base_targets_raw.resize(curr_cam_res[1:]) + state.base_targets.resize(curr_cam_res[1:]) + state.base_bkgs.resize(curr_cam_res[1:]) + if state.has_masks: + state.base_masks.resize(curr_cam_res[1:]) + return True + else: + return False + #END if rescale velocity + + + def render_sequence_val(sequence, vel_pad, it): + log.debug("Render validation images for sequence, iteration %d", it) + with profiler.sample('render validation'): + for state in sequence: + log.debug("Render density validation frame %d", state.frame) + dens_transform = state.get_density_transform() + val_imgs = renderer.render_density_SDF_switch(dens_transform, lights, val_cameras) + renderer.write_images_batch_views(val_imgs, 'val_img_b{batch:04d}_cam{view:02d}_{idx:04d}', base_path=state.data_path, frame_idx=it, image_format='PNG') + + slc = dens_transform.data[...,dens_transform.grid_size[-1]//2,:] + slc = _slice_single_channel_color_transfer(slc) + renderer.write_images_batch_views([slc], 'val_slc_b{batch:04d}_camX{view:02d}_{idx:04d}', base_path=state.data_path, frame_idx=it, image_format="EXR") + + #render_slices(dens_transform.data, ["X"], state.data_path, name_pre="val_slc", format="EXR", normalize=False, slice_indices=[dens_transform.grid_size[-1]//2]) + + vel_transform = state.get_velocity_transform() + vel_scale = vel_transform.cell_size_world().value + log.debug("Render velocity validation frame %d with scale %s", state.frame, vel_scale) + #sim_transform.set_data(vel_pad) + vel_centered = state.velocity.centered() * get_vel_scale_for_render(setup, vel_transform)#vel_scale/float(setup.data.step)*setup.rendering.velocity_scale # + val_imgs = vel_renderer.render_density(vel_transform, [tf.abs(vel_centered)], val_cameras) + renderer.write_images_batch_views(val_imgs, 'val_velA_b{batch:04d}_cam{view:02d}_{idx:04d}', base_path=state.data_path, frame_idx=it, image_format='EXR') + # val_imgs = vel_renderer.render_density(vel_transform, [tf.maximum(vel_centered, 0)], val_cameras) + # vel_renderer.write_images([tf.concat(val_imgs, axis=0)], ['val_velP_cam{}_{:04d}'], base_path=state.data_path, use_batch_id=True, frame_id=it, format='PNG') + # val_imgs = vel_renderer.render_density(vel_transform, [tf.maximum(-vel_centered, 0)], val_cameras) + # vel_renderer.write_images([tf.concat(val_imgs, axis=0)], ['val_velN_cam{}_{:04d}'], base_path=state.data_path, use_batch_id=True, frame_id=it, format='PNG') + + if True: #Debug + # render hull + pass + + def render_sequence_val_DEBUG(sequence, vel_pad, it): + log.debug("Render validation images for sequence, iteration %d", it) + with profiler.sample('render validation'): + for state in sequence: + log.debug("Render density validation frame %d", state.frame) + dens_transform = state.get_density_transform() + val_imgs = renderer.render_density_SDF_switch(dens_transform, lights, val_cameras) + val_imgs = [_.numpy() for _ in val_imgs] + #renderer.write_images_batch_views(val_imgs, 'val_img_b{batch:04d}_cam{view:02d}_{idx:04d}', base_path=state.data_path, frame_idx=it, image_format='PNG') + + vel_transform = state.get_velocity_transform() + vel_scale = vel_transform.cell_size_world().value + log.debug("Render velocity validation frame %d with scale %s", state.frame, vel_scale) + vel_centered = state.velocity.centered() * get_vel_scale_for_render(setup, vel_transform) + val_imgs = vel_renderer.render_density(vel_transform, [tf.abs(vel_centered)], val_cameras) + val_imgs = [_.numpy() for _ in val_imgs] + #renderer.write_images_batch_views(val_imgs, 'val_velA_b{batch:04d}_cam{view:02d}_{idx:04d}', base_path=state.data_path, frame_idx=it, image_format='PNG') + + + # print growing stats + def log_growth(tar_shape, intervals, factor, max_iter, name): + s = "Growing {}: {:d} steps with factor {:f}".format(name, len(intervals)+1, factor) + abs_intervals = abs_grow_intervals(intervals, max_iter) + if abs_intervals[-1][0]>abs_intervals[-1][1]: + log.warning("Insufficient iterations for all grow intervals") + for interval in abs_intervals: + shape = current_grow_shape(tar_shape, interval[0], factor, intervals) + s += "\n\t[{:d},{:d}] {}".format(interval[0], interval[1]-1, shape) + log.info(s) + if setup.training.density.pre_opt.first.iterations>0: + if setup.training.density.pre_opt.first.grow.intervals: + log_growth(main_opt_start_vel_shape, setup.training.density.pre_opt.first.grow.intervals, setup.training.density.pre_opt.first.grow.factor, setup.training.density.pre_opt.first.iterations, 'pre-opt density') + if setup.training.velocity.pre_opt.first.iterations>0: + if setup.training.velocity.pre_opt.first.grow.intervals: + log_growth(main_opt_start_vel_shape, setup.training.velocity.pre_opt.first.grow.intervals, setup.training.velocity.pre_opt.first.grow.factor, setup.training.velocity.pre_opt.first.iterations, 'pre-opt velocity') + if setup.training.density.grow.intervals: + log_growth(base_shape, setup.training.density.grow.intervals, setup.training.density.grow.factor, setup.training.iterations, 'density') + if setup.training.velocity.grow.intervals: + log_growth(base_shape, setup.training.velocity.grow.intervals, setup.training.velocity.grow.factor, setup.training.iterations, 'velocity') + + loss_schedules = LossSchedules( \ + density_target = make_schedule(setup.training.density.preprocessed_target_loss), + density_target_raw = make_schedule(setup.training.density.raw_target_loss), + density_target_vol = make_schedule(setup.training.density.volume_target_loss), + density_proxy_vol = make_schedule(setup.training.density.volume_proxy_loss), + density_target_depth_smoothness = make_schedule(setup.training.density.target_depth_smoothness_loss), + density_negative = make_schedule(setup.training.density.negative), + density_hull = make_schedule(setup.training.density.hull), + density_smoothness = make_schedule(setup.training.density.smoothness_loss), + density_smoothness_2 = make_schedule(setup.training.density.smoothness_loss_2), + density_smoothness_temporal = make_schedule(setup.training.density.temporal_smoothness_loss), + density_warp = make_schedule(setup.training.density.warp_loss), + density_disc = make_schedule(setup.training.density.discriminator_loss), + density_center = make_schedule(setup.training.density.center_loss), + SDF_target_pos = make_schedule(setup.training.density.SDF_pos_loss), + + velocity_target_vol = make_schedule(setup.training.velocity.volume_target_loss), + velocity_warp_dens = make_schedule(setup.training.velocity.density_warp_loss), + velocity_warp_dens_proxy = make_schedule(setup.training.velocity.density_proxy_warp_loss), + velocity_warp_dens_target = make_schedule(setup.training.velocity.density_target_warp_loss), + velocity_warp_vel = make_schedule(setup.training.velocity.velocity_warp_loss), + velocity_divergence = make_schedule(setup.training.velocity.divergence_loss), + velocity_smoothness = make_schedule(setup.training.velocity.smoothness_loss), + velocity_cossim = make_schedule(setup.training.velocity.cossim_loss), + velocity_magnitude = make_schedule(setup.training.velocity.magnitude_loss), + velocity_CFLcond = make_schedule(setup.training.velocity.CFL_loss), + velocity_MS_coherence = make_schedule(setup.training.velocity.MS_coherence_loss), + + density_lr = make_schedule(setup.training.density.learning_rate), + light_lr = make_schedule(setup.training.light.learning_rate), + velocity_lr = make_schedule(setup.training.velocity.learning_rate), + discriminator_lr = make_schedule(setup.training.discriminator.learning_rate), + view_encoder_regularization = make_schedule(setup.training.density.regularization), + density_decoder_regularization = make_schedule(setup.training.density.regularization), + velocity_decoder_regularization = make_schedule(setup.training.velocity.regularization), + discriminator_regularization = make_schedule(setup.training.discriminator.regularization), + + velocity_warp_dens_MS_weighting = make_schedule(setup.training.velocity.density_warp_loss_MS_weighting), + velocity_warp_dens_tar_MS_weighting = make_schedule(setup.training.velocity.density_target_warp_loss_MS_weighting), + velocity_divergence_MS_weighting = make_schedule(setup.training.velocity.divergence_loss_MS_weighting), + velocity_CFLcond_MS_weighting = make_schedule(setup.training.velocity.CFL_loss_MS_weighting), + velocity_MS_coherence_MS_weighting = make_schedule(setup.training.velocity.MS_coherence_loss_MS_weighting) + + ) + + + light_lr = tf.Variable(initial_value=scalar_schedule(setup.training.light.learning_rate, 0), dtype=tf.float32, name='light_lr', trainable=False) + light_optimizer = tf.train.AdamOptimizer(light_lr, beta1=setup.training.light.optim_beta) + dens_lr = tf.Variable(initial_value=scalar_schedule(setup.training.density.learning_rate, 0), dtype=tf.float32, name='density_lr', trainable=False) + dens_optimizer = tf.train.AdamOptimizer(dens_lr, beta1=setup.training.density.optim_beta) #, epsilon=1e-3 + vel_lr = tf.Variable(initial_value=scalar_schedule(setup.training.velocity.learning_rate, 0), dtype=tf.float32, name='velocity_lr', trainable=False) + vel_optimizer = tf.train.AdamOptimizer(vel_lr, beta1=setup.training.velocity.optim_beta) + disc_lr = tf.Variable(initial_value=scalar_schedule(setup.training.discriminator.learning_rate, 0), dtype=tf.float32, name='discriminator_lr', trainable=False) + disc_optimizer = tf.train.AdamOptimizer(disc_lr, beta1=setup.training.discriminator.optim_beta) + + # growing unet + grow_lifting_fade_layers = setup.training.density.grow_lifting_skip is not None + if grow_lifting_fade_layers: + num_lifting_layers = len(setup.training.density.grow_lifting_skip) + assert setup.training.density.grow_lifting_train is not None + assert len(setup.training.density.grow_lifting_train)==num_lifting_layers + assert setup.training.density.grow_lifting_lr is not None + assert len(setup.training.density.grow_lifting_lr)==num_lifting_layers + assert lifting_network_model.num_levels==num_lifting_layers + + grow_lifting_skip_schedules = [make_schedule(setup.training.density.grow_lifting_skip[level]) for level in range(num_lifting_layers)] + grow_lifting_train_schedules = [make_schedule(setup.training.density.grow_lifting_train[level]) for level in range(num_lifting_layers)] + grow_lifting_lr_schedules = [make_schedule(setup.training.density.grow_lifting_lr[level]) for level in range(num_lifting_layers)] + + for level in range(num_lifting_layers): + plot_schedule(setup.training.density.grow_lifting_lr[level], setup.training.iterations, os.path.join(setup.paths.config, 'lift_lr_%d.png'%level), 'Lifting %d LR'%level) + plot_schedule(setup.training.density.grow_lifting_skip[level], setup.training.iterations, os.path.join(setup.paths.config, 'lift_skip_%d.png'%level), 'Lifting %d skip'%level) + + grow_lifting_lr = [tf.Variable(initial_value=s(0), dtype=tf.float32, name='light_lr', trainable=False) for s in grow_lifting_lr_schedules] + grow_lifting_optimizers = [tf.train.AdamOptimizer(lr, beta1=setup.training.density.optim_beta) for level, lr in enumerate(grow_lifting_lr)] + + grow_lifting_fade_residual = setup.training.density.grow_lifting_residual is not None + if grow_lifting_fade_residual: + num_lifting_layers = len(setup.training.density.grow_lifting_residual) + assert lifting_network_model is not None + assert lifting_network_model.num_levels==num_lifting_layers + assert lifting_network_model.output_mode=="RESIDUAL_WEIGHTED" + + grow_lifting_residual_schedules = [make_schedule(setup.training.density.grow_lifting_residual[level]) for level in range(num_lifting_layers)] + for level in range(num_lifting_layers): + plot_schedule(setup.training.density.grow_lifting_residual[level], setup.training.iterations, os.path.join(setup.paths.config, 'lift_residual_%d.png'%level), 'Lifting %d residual'%level) + + grow_volenc_fade_residual = setup.training.density.grow_volenc_residual is not None + if grow_volenc_fade_residual: + num_lifting_layers = len(setup.training.density.grow_volenc_residual) + assert volume_encoder_model is not None + assert volume_encoder_model.num_levels==num_lifting_layers + assert volume_encoder_model.output_mode=="RESIDUAL_WEIGHTED" + + grow_volenc_residual_schedules = [make_schedule(setup.training.density.grow_volenc_residual[level]) for level in range(num_lifting_layers)] + for level in range(num_lifting_layers): + plot_schedule(setup.training.density.grow_volenc_residual[level], setup.training.iterations, os.path.join(setup.paths.config, 'volenc_residual_%d.png'%level), 'VolEnc %d residual'%level) + + grow_vel_MS_residual = setup.training.velocity.decoder.recursive_MS_residual_weight is not None + if grow_vel_MS_residual: + assert get_max_recursive_MS_grow_levels(setup.training.velocity.decoder)==len(setup.training.velocity.decoder.recursive_MS_residual_weight) + + grow_vel_MS_residual_schedules = [make_schedule(setup.training.velocity.decoder.recursive_MS_residual_weight[level]) for level in range(num_lifting_layers)] + for level in range(num_lifting_layers): + plot_schedule(setup.training.velocity.decoder.recursive_MS_residual_weight[level], setup.training.iterations, os.path.join(setup.paths.config, 'vel_MS_residual_%d.png'%level), 'Vel MS %d residual'%level) + + opt_ckpt = tf.train.Checkpoint(dens_optimizer=dens_optimizer, vel_optimizer=vel_optimizer, disc_optimizer=disc_optimizer) + + main_ctx = OptimizationContext(setup=setup, iteration=0, loss_schedules=loss_schedules, \ + rendering_context=main_render_ctx, vel_scale=[1,1,1], warp_order=setup.training.velocity.warp_order, dt=1.0, buoyancy=buoyancy, \ + dens_warp_clamp=setup.training.density.warp_clamp, vel_warp_clamp=setup.training.velocity.warp_clamp, \ + density_optimizer=dens_optimizer, density_lr=dens_lr, light_optimizer=light_optimizer, light_lr=light_lr, \ + velocity_optimizer=vel_optimizer, velocity_lr=vel_lr, \ + frame=None, tf_summary=summary, summary_interval=25, summary_pre=None, profiler=profiler, + light_var_list=light_var_list, allow_MS_losses=setup.training.allow_MS_losses, norm_spatial_dims=True) + + main_ctx.set_loss_func("density/target", setup.training.density.error_functions.preprocessed_target_loss) + main_ctx.set_loss_func("density/target_raw", setup.training.density.error_functions.raw_target_loss) + main_ctx.set_loss_func("density/target_vol", setup.training.density.error_functions.volume_target_loss) + main_ctx.set_loss_func("density/proxy_vol", setup.training.density.error_functions.volume_proxy_loss) + main_ctx.set_loss_func("density/target_depth_smooth", setup.training.density.error_functions.target_depth_smoothness_loss) + main_ctx.set_loss_func("density/hull", setup.training.density.error_functions.hull) + main_ctx.set_loss_func("density/negative", setup.training.density.error_functions.negative) + main_ctx.set_loss_func("density/edge", setup.training.density.error_functions.smoothness_loss) + main_ctx.set_loss_func("density/smooth", setup.training.density.error_functions.smoothness_loss_2) + main_ctx.set_loss_func("density/smooth-temp", setup.training.density.error_functions.temporal_smoothness_loss) + main_ctx.set_loss_func("density/warp", setup.training.density.error_functions.warp_loss) + main_ctx.set_loss_func("density/center", setup.training.density.error_functions.center_loss) + main_ctx.set_loss_func("density/target_pos", setup.training.density.error_functions.SDF_pos_loss) + + main_ctx.set_loss_func("velocity/target_vol", setup.training.velocity.error_functions.volume_target_loss) + main_ctx.set_loss_func("velocity/density_warp", setup.training.velocity.error_functions.density_warp_loss) + main_ctx.set_loss_func("velocity/densProxy_warp", setup.training.velocity.error_functions.density_proxy_warp_loss) + main_ctx.set_loss_func("velocity/densTar_warp", setup.training.velocity.error_functions.density_target_warp_loss) + main_ctx.set_loss_func("velocity/velocity_warp", setup.training.velocity.error_functions.velocity_warp_loss) + main_ctx.set_loss_func("velocity/divergence", setup.training.velocity.error_functions.divergence_loss) + main_ctx.set_loss_func("velocity/magnitude", setup.training.velocity.error_functions.magnitude_loss) + main_ctx.set_loss_func("velocity/CFL", setup.training.velocity.error_functions.CFL_loss) + main_ctx.set_loss_func("velocity/MS_coherence", setup.training.velocity.error_functions.MS_coherence_loss) + + #gradient warping: + main_ctx.update_first_dens_only = make_schedule(setup.training.density.warp_gradients.update_first_only) + main_ctx.warp_dens_grads = make_schedule(setup.training.density.warp_gradients.active) + main_ctx.warp_dens_grads_decay = make_schedule(setup.training.density.warp_gradients.decay) + main_ctx.warp_vel_grads = make_schedule(setup.training.velocity.warp_gradients.active) + main_ctx.warp_vel_grads_decay = make_schedule(setup.training.velocity.warp_gradients.decay) + main_ctx.custom_dens_grads_weight = make_schedule(setup.training.density.warp_gradients.weight) + main_ctx.custom_vel_grads_weight = make_schedule(setup.training.velocity.warp_gradients.weight) + + main_ctx.target_weights = view_interpolation_target_weights + log.info("Target weights: %s", view_interpolation_target_weights) + + sF_render_ctx = copy.copy(main_render_ctx) + sF_render_ctx.cameras = None #scalarFlow_cameras + opt_ctx = copy.copy(main_ctx) + opt_ctx.render_ctx = sF_render_ctx + + if setup.training.density.scale_render_grads_sharpness>0.0: + log.info("Scaling density render gradients with exisiting density distribution.") + opt_ctx.add_render_op('DENSITY', opt_ctx.RO_grid_dens_grad_scale(weight=1, sharpness=setup.training.density.scale_render_grads_sharpness, eps=1e-5)) + + if setup.training.discriminator.active: + disc_render_ctx = copy.copy(main_render_ctx) + disc_render_ctx.cameras = disc_cameras + #log.info("Disc cam jitter: %s", [_.jitter for _ in disc_render_ctx.cameras]) + #log.warning("Full discriminator in/out debugging enabled!") + disc_debug_path = os.path.join(setup.paths.data, 'disc_debug') + os.makedirs(disc_debug_path) + disc_ctx = DiscriminatorContext(ctx=opt_ctx, model=disc_model, rendering_context=disc_render_ctx, real_data=disc_real_data, \ + loss_type=setup.training.discriminator.loss_type, optimizer=disc_optimizer, learning_rate=disc_lr, \ + crop_size=disc_in_shape[:-1], scale_range=setup.data.discriminator.scale_range, rotation_mode=setup.data.discriminator.rotation_mode, \ + check_input=DiscriminatorContext.CHECK_INPUT_RAISE_NOTFINITE | DiscriminatorContext.CHECK_INPUT_CHECK_NOTFINITE | DiscriminatorContext.CHECK_INPUT_CLAMP | \ + (DiscriminatorContext.CHECK_INPUT_SIZE if setup.training.discriminator.use_fc is not None else 0x0), \ + check_info_path=disc_debug_path, resource_device=data_device, \ + scale_samples_to_input_resolution=setup.data.discriminator.scale_input_to_crop, \ + use_temporal_input=setup.training.discriminator.temporal_input.active, temporal_input_steps=disc_input_steps, \ + cam_x_range=[-27,-7] if setup.data.discriminator.density_type=="SF" else [-10,10]) #SF cams look up about 17 deg + #disc_ctx.train = train_disc + if make_disc_dataset and disc_dump_samples: + disc_ctx.dump_path = os.path.join(setup.paths.data, 'disc_samples') + log.warning("Dumping ALL discriminator samples to %s.", disc_ctx.dump_path) + os.makedirs(disc_ctx.dump_path) + log.info("Discriminator input shape: %s, res: %s", disc_ctx.model.input_shape, disc_ctx.input_res) + else: + disc_ctx = DiscriminatorContext(opt_ctx, None, main_render_ctx, None, "SGAN", None, disc_lr) + disc_ctx.train = False + + dump_samples = False + val_out_step = setup.validation.output_interval + out_step = setup.training.summary_interval + loss_summary = [] + + class StopTraining(Exception): + pass + + def check_loss_summary(loss_summaries, total_losses, it, gradients=None, grad_max=None): + # check losses and gradients for NaN/Inf + for f, f_item in loss_summaries.items(): + for k, k_item in f_item.items(): + if not np.all(np.isfinite(k_item)): + raise ValueError("Loss summary {} of frame {} is not finite.".format(k,f)) + if total_losses and not np.all(np.isfinite(total_losses)): + raise ValueError("Combined losses are not finite.".format(k,f)) + if gradients is not None: + for f, f_item in gradients.items(): + for k, k_item in f_item.items(): + if not np.all(np.isfinite(k_item)): + raise ValueError("Gradient summary {} of frame {} is not finite.".format(k,f)) + if grad_max is not None: + if "density/light" in k or "velocity/buoyancy" in k or "-v" in k: + continue # gradients of gloabl scalars are higher + if np.any(np.greater(k_item, grad_max)): + #raise ValueError("Gradient summary {} of frame {} is greater than {}.".format(k,f, grad_max)) + log.warning("Gradient summary {} of frame {} is greater than {}.".format(k,f, grad_max)) + + def get_total_scaled_loss(loss_summaries): + return sum(loss[-3] for f in loss_summaries for n, loss in loss_summaries[f].items()) + + def get_loss_scale(loss_summaries, loss_frames, loss_name): + for idx in range(len(loss_frames)): + if loss_name in loss_summaries[loss_frames[idx]]: + return loss_summaries[loss_frames[idx]][loss_name][-1] + return 0.0 + + + def print_loss_summary(loss_summaries, total_losses, start_time, last_time, it, iterations, gradients=None, regularization_stats=None): + ''' + numpy scalars: + loss_summaries: {: {: (scaled, raw, scale), ...}, ...} + gradients: {: {: [grad, ...], ...}, ...} + ''' + log.info('RAM: current: %d MiB', psutil.Process(os.getpid()).memory_info().rss/(1024*1024)) + log.info('GPU mem: current: %d MiB, max: %d MiB, limit: %d MiB', \ + tf.contrib.memory_stats.BytesInUse().numpy().tolist()/(1024*1024), \ + tf.contrib.memory_stats.MaxBytesInUse().numpy().tolist()/(1024*1024), \ + tf.contrib.memory_stats.BytesLimit().numpy().tolist()/(1024*1024)) + s = ["--- Loss Summary ---\n"] + now = time.time() + avg = (now-start_time)/max(1,it) + avg_last = (now-last_time)/out_step + s.append('Timing: elapsed {}, avg/step: total {}, last {}, remaining: total {}, last {}\n'.format(format_time(now-start_time), format_time(avg), format_time(avg_last), format_time(avg*(iterations-it)), format_time(avg_last*(iterations-it)))) + s.append('{:26}(x{:>11}): {:>11}({:>11})| ...\n'.format('Last losses', 'Scale', 'Scaled', 'Raw')) + loss_names = sorted({n for f in loss_summaries for n in loss_summaries[f]}) + loss_frames = sorted((f for f in loss_summaries)) + #loss_scales = [loss_summaries[loss_frames[0]][k][-1] for k in loss_names] + loss_scales = [get_loss_scale(loss_summaries, loss_frames, k) for k in loss_names] + for key, scale in zip(loss_names, loss_scales): + s.append('{:<26}(x{: 10.04e}):'.format(key, scale)) + # loss_values = active_losses[key]['frames'] + for f in loss_frames: + if key in loss_summaries[f]: + s.append(' {: 10.04e}({: 10.04e})'.format(loss_summaries[f][key][-3], loss_summaries[f][key][-2])) + else: + s.append(' {:>11}({:>11})'.format('N/A', 'N/A')) + s.append('|') + s.append('\n') + if gradients is not None: + s.append('{:26}: {:>11}| ...\n'.format("Per-loss volume gradients", "mean-abs")) + grad_names = sorted({n for f in gradients for n in gradients[f]}) + grad_frames = sorted((f for f in gradients)) + for key in grad_names: + s.append('{:<26}:'.format(key)) + #grad_values = active_losses[key]['frames_grad'] + for f in grad_frames: + if key in gradients[f]: + s.append(','.join([' {: 10.04e}']*len(gradients[f][key])).format(*gradients[f][key])) + else: + s.append(' {:>11}'.format('N/A')) + s.append('|') + s.append('\n') + s.append('total scaled loss (dens, vel):') + for total_loss in total_losses: + s.append(' ({: 10.04e},{: 10.04e}),'.format(*total_loss)) + s.append('\n') + if regularization_stats is not None: + s.append('-- Regularization Stats --\n') + for network_name, stats in regularization_stats.items(): + s.append('\t- {} (count: {}, weight: {}, applied: {}): mean, max -\n'.format(network_name, stats["Gradient count"], stats["weight"], stats["applied"])) + s.append('Weights: ') + for vmean, vmax in stats["Weights"]: + s.append('{: 10.04e},{: 10.04e}|'.format(vmean,vmax)) + s.append('\n') + s.append('Loss grad: '.format()) + for vmean, vmax in stats["Loss gradients"]: + s.append('{: 10.04e},{: 10.04e}|'.format(vmean,vmax)) + s.append('\n') + s.append('Reg grad: '.format()) + for vmean, vmax in stats["Regularization gradients"]: + s.append('{: 10.04e},{: 10.04e}|'.format(vmean,vmax)) + s.append('\n') + s.append('lr: dens {: 10.04e}, vel {: 10.04e}'.format(dens_lr.numpy(), vel_lr.numpy())) + log.info(''.join(s)) + + def print_disc_summary(disc_ctx, disc_loss):#_real, disc_scores_real, disc_loss_fake, disc_scores_fake): + loss_summaries = [disc_ctx.opt_ctx.pop_loss_summary()] + active_losses = {} + f = 0 + for summ in loss_summaries: + for k, e in summ.items(): + if k not in active_losses: + active_losses[k] = {'frames':{}, 'scale':e[-1] if e[-1] is not None else 1.0} + active_losses[k]['frames'][f] = (e[-3], e[-2] if e[-2] is not None else e[-3]) + f +=1 + s = ["--- Disc Summary ---\n"] + s.append('{:26}(x{:>9}): {:>11}({:>11}), ...\n'.format('Last losses', 'Scale', 'Scaled', 'Raw')) + for key in sorted(active_losses.keys()): + s.append('{:<26}(x{: 9.06f}):'.format(key, active_losses[key]['scale'])) + loss_values = active_losses[key]['frames'] + for i in range(f): + if i in loss_values: + s.append(' {: 10.04e}({: 10.04e}),'.format(*loss_values[i])) + else: + s.append(' {:>11}({:>11}),'.format('N/A', 'N/A')) + s.append('\n') + if len(disc_loss)==4: + s.append('Total loss (scores): real {:.06f} ({}), fake {:.06f} ({})\n'.format(disc_loss[0], disc_loss[2], disc_loss[1], disc_loss[3])) + elif len(disc_loss)==2: + s.append('Total loss (scores): {:.06f} ({})\n'.format(*disc_loss)) + s.append('lr: {:.08f}'.format(disc_ctx.lr.numpy())) + if setup.training.discriminator.history.samples>0: + s.append(', history size: {}'.format(len(disc_ctx.history))) + log.info(''.join(s)) + + max_ckp = 4 + next_ckp = 0 + def save_checkpoint(name=None): + if name is None: + global next_ckp + name = str(next_ckp) + next_ckp = (next_ckp+1)%max_ckp + log.info("Save checkpoint '%s'", name) + sequence.save() + if setup.training.discriminator.active: + save_NNmodel(disc_ctx.model, 'disc_ckp', setup.paths.data) + #if setup.training.discriminator.history.save: + # disc_ctx.history.serialize(setup.paths.data) + if (setup.training.density.decoder.active or setup.training.velocity.decoder.active) and target_encoder_model is not None: + save_NNmodel(target_encoder_model, 'target_encoder_ckp-%s'%name, setup.paths.data) + if lifting_network_model is not None: + save_NNmodel(lifting_network_model, 'lifting_network_ckp-%s'%name, setup.paths.data) + if setup.training.volume_encoder.active and volume_encoder_model is not None: + save_NNmodel(volume_encoder_model, 'volume_encoder_ckp-%s'%name, setup.paths.data) + if setup.training.frame_merge_network.active and frame_merge_network_model is not None: + save_NNmodel(frame_merge_network_model, 'frame_merge_network_ckp-%s'%name, setup.paths.data) + if setup.training.density.decoder.active and density_decoder_model is not None: + save_NNmodel(density_decoder_model, 'density_decoder_ckp-%s'%name, setup.paths.data) + if setup.training.velocity.decoder.active and velocity_decoder_model is not None: + save_NNmodel(velocity_decoder_model, 'velocity_decoder_ckp-%s'%name, setup.paths.data) + if setup.training.velocity.decoder.active and vel_input_encoder_model is not None: + save_NNmodel(vel_input_encoder_model, 'velocity_input_encoder_ckp-%s'%name, setup.paths.data) + if setup.training.velocity.decoder.active and vel_downscale_encoder_model is not None: + save_NNmodel(vel_downscale_encoder_model, 'velocity_downscale_encoder_ckp-%s'%name, setup.paths.data) + + + + def dump_inputs(sequence, idx=0): + for state in sequence: + state.base_targets_raw.save_scaled(renderer, state.data_path, "PNG", name="dump_targetraw_%d"%(idx,)) + state.base_targets.save_scaled(renderer, state.data_path, "PNG", name="dump_target_%d"%(idx,)) + state.base_bkgs.save_scaled(renderer, state.data_path, "PNG", name="dump_bkg_%d"%(idx,)) + if state.has_masks: + state.base_masks.save_scaled(renderer, state.data_path, "PNG", name="dump_mask_%d"%(idx,)) + + def dump_vel_input_features(sequence, idx=0): + for state in sequence: + vel_transform = state.get_velocity_transform() + vel_inp = state.get_volume_features(setup.training.velocity.decoder.type_input_features) + shape = GridShape.from_tensor(vel_inp) + for vel_inp_channel in tf.split(vel_inp, shape.c, axis=-1): + val_imgs = vel_renderer.render_density(vel_transform, [tf.maximum(vel_inp_channel, 0)], val_cameras) + #vel_renderer.write_images([tf.concat(val_imgs, axis=0)], ['P_inp_velP_cam{}'], base_path=state.data_path, use_batch_id=True, format='PNG') + vel_renderer.write_images_batch_views(val_imgs, 'P_trainP_velInpP_{batch:04d}_cam{view:02d}_{idx:04d}', base_path=state.data_path, frame_idx=idx, image_format='PNG') + + def dump_dens_input_features(sequence, idx=0): + for state in sequence: + dens_transform = state.get_density_transform() + dens_inp = state.get_volume_features(setup.training.density.decoder.type_input_features) + shape = GridShape.from_tensor(dens_inp) + for i, dens_inp_channel in enumerate(tf.split(dens_inp, shape.c, axis=-1)): + val_imgs = vel_renderer.render_density(dens_transform, [tf.maximum(dens_inp_channel, 0)], val_cameras) + #vel_renderer.write_images([tf.concat(val_imgs, axis=0)], ['P_inp_velP_cam{}'], base_path=state.data_path, use_batch_id=True, format='PNG') + vel_renderer.write_images_batch_views(val_imgs, 'P_trainP_velInpP{:02d}'.format(i)+'_{batch:04d}_cam{view:02d}_{idx:04d}', base_path=state.data_path, frame_idx=idx, image_format='PNG') + + + # scene serialization + scene = { + "cameras":cameras, + "sFcameras":scalarFlow_cameras, + "lighting":lights, + "objects":[sim_transform], + "pre_opt_vel_shape": main_opt_start_vel_shape, + "pre_opt_dens_shape": main_opt_start_dens_shape, + "vel_shape": base_shape, + "vel_shape": base_shape, + } + scene_file = os.path.join(setup.paths.config, "scene.json") + #log.debug("Serializing scene to %s ...", scene_file) + with open(scene_file, "w") as file: + try: + json.dump(scene, file, default=tf_to_dict, sort_keys=True)#, indent=2) + except: + log.exception("Scene serialization failed.") + + except KeyboardInterrupt: + log.warning("Interrupt during setup.") + sys.exit(0) + except: + log.exception('Exception during setup:') + sys.exit(1) + +# --- Optimization --- + signal.signal(signal.SIGINT, handle_train_interrupt) + optim_start = time.time() + try: + with summary_writer.as_default(), summary.always_record_summaries(): + + opt_ctx.summary_pre = "Main-Optim" + #run full-sequence optimization + log.info('--- Sequence optimization (order: %s) start (%d - %d iterations) ---', setup.training.frame_order, setup.training.start_iteration, setup.training.iterations) + loss_schedules.set_schedules( \ + density_target = make_schedule(setup.training.density.preprocessed_target_loss), + density_target_raw = make_schedule(setup.training.density.raw_target_loss), + density_target_vol = make_schedule(setup.training.density.volume_target_loss), + density_proxy_vol = make_schedule(setup.training.density.volume_proxy_loss), + density_target_depth_smoothness = make_schedule(setup.training.density.target_depth_smoothness_loss), + density_hull = make_schedule(setup.training.density.hull), + density_negative = make_schedule(setup.training.density.negative), + density_smoothness = make_schedule(setup.training.density.smoothness_loss), + density_smoothness_2 = make_schedule(setup.training.density.smoothness_loss_2), + density_smoothness_temporal = make_schedule(setup.training.density.temporal_smoothness_loss), + density_warp = make_schedule(setup.training.density.warp_loss), + density_disc = make_schedule(setup.training.density.discriminator_loss), + density_center = make_schedule(setup.training.density.center_loss), + SDF_target_pos = make_schedule(setup.training.density.SDF_pos_loss), + + velocity_target_vol = make_schedule(setup.training.velocity.volume_target_loss), + velocity_warp_dens = make_schedule(setup.training.velocity.density_warp_loss), + velocity_warp_dens_proxy = make_schedule(setup.training.velocity.density_proxy_warp_loss), + velocity_warp_dens_target = make_schedule(setup.training.velocity.density_target_warp_loss), + velocity_warp_vel = make_schedule(setup.training.velocity.velocity_warp_loss), + velocity_divergence = make_schedule(setup.training.velocity.divergence_loss), + velocity_smoothness = make_schedule(setup.training.velocity.smoothness_loss), + velocity_cossim = make_schedule(setup.training.velocity.cossim_loss), + velocity_magnitude = make_schedule(setup.training.velocity.magnitude_loss), + velocity_CFLcond = make_schedule(setup.training.velocity.CFL_loss), + velocity_MS_coherence = make_schedule(setup.training.velocity.MS_coherence_loss), + + density_lr = make_schedule(setup.training.density.learning_rate), + velocity_lr = make_schedule(setup.training.velocity.learning_rate), + discriminator_lr = make_schedule(setup.training.discriminator.learning_rate), + density_decoder_train = make_schedule(setup.training.density.train_decoder), + velocity_decoder_train = make_schedule(setup.training.velocity.train_decoder), + frame_encoders_train = make_schedule(setup.training.train_frame_encoders), + + view_encoder_regularization = make_schedule(setup.training.density.regularization), + density_decoder_regularization = make_schedule(setup.training.density.regularization), + velocity_decoder_regularization = make_schedule(setup.training.velocity.regularization), + discriminator_regularization = make_schedule(setup.training.discriminator.regularization), + + velocity_warp_dens_MS_weighting = make_schedule(setup.training.velocity.density_warp_loss_MS_weighting), + velocity_warp_dens_tar_MS_weighting = make_schedule(setup.training.velocity.density_target_warp_loss_MS_weighting), + velocity_divergence_MS_weighting = make_schedule(setup.training.velocity.divergence_loss_MS_weighting), + velocity_CFLcond_MS_weighting = make_schedule(setup.training.velocity.CFL_loss_MS_weighting), + velocity_MS_coherence_MS_weighting = make_schedule(setup.training.velocity.MS_coherence_loss_MS_weighting), + + sequence_length = make_schedule(setup.training.sequence_length) + + ) + + velocity_noise_schedule = make_schedule(setup.training.velocity.noise_std) + def seq_vel_add_noise(opt_ctx, seq): + vel_noise_std = velocity_noise_schedule(opt_ctx.iteration) + if opt_ctx.LA(vel_noise_std): + log.debug("Add noise to sequence velocity: std: %f, it: %d.", vel_noise_std, opt_ctx.iteration) #TODO debug + for state in seq: + v = state.velocity + v.assign_add( \ + x = tf.random.normal([1]+v.x_shape+[1], stddev=vel_noise_std, dtype=tf.float32), \ + y = tf.random.normal([1]+v.y_shape+[1], stddev=vel_noise_std, dtype=tf.float32), \ + z = tf.random.normal([1]+v.z_shape+[1], stddev=vel_noise_std, dtype=tf.float32)) + #def optimize_sequence(opt_ctx, state, iterations, use_vel=False, disc_ctx=None, dens_factor=1.0, dens_intervals=[], vel_factor=1.0, vel_intervals=[], start_iteration=0): + with profiler.sample('Main Optimization'), tf.device(compute_device): + def regualrization_gradient_summary(model, weight=1.0, apply=True, stats_dict=None, name="Network"): + if model is not None: + if isinstance(model, list): + for i, m in enumerate(model): + regualrization_gradient_summary(m, weight, apply, stats_dict, name=name+"_L%d"%(i,)) + elif stats_dict is not None: + stats_dict[name] = {} + stats_dict[name]["weight"] = weight + stats_dict[name]["applied"] = apply + stats_dict[name]["Weights"] = model.get_weights_summary() + stats_dict[name]["Gradient count"] = model.num_pending_gradients + stats_dict[name]["Loss gradients"] = model.get_pending_gradients_summary() + reg_grads = model.compute_regularization_gradients(weight=weight, add_gradients=apply) + stats_dict[name]["Regularization gradients"] = [[tf.reduce_mean(_).numpy(), tf.reduce_max(tf.abs(_)).numpy()] for _ in reg_grads] + elif apply: + model.compute_regularization_gradients(weight=weight, add_gradients=apply) + + def scale_grads_for_smoothing(grads_vars, batch_group_size): + sf = tf.constant(1/batch_group_size, dtype=tf.float32) #smoothing factor + scaled_grads_vars = [(g*sf, v) for g, v in grads_vars] + return scaled_grads_vars + + def DEBUG_check_duplicate_vars(grads_vars): + seen_vars = set() + duplicates = set() + for g, v in grads_vars: + if v in seen_vars: + duplicates.add(v) + seen_vars.add(v) + if len(duplicates)>0: + raise RuntimeError("%d duplicate variables in grads_vars."%(len(duplicates),)) + + disc_ctx.train = setup.training.discriminator.train and setup.training.discriminator.active + inspect_gradients_list = {state.frame:{} for state in sequence} + if setup.training.density.pre_opt.inspect_gradients==1: + def ig_func(opt_ctx, gradients, name): + if name.endswith('_x') or name.endswith('_y') or name.endswith('_z'): + #velocity gradients are given individually per component with name=loss_name + _(x|y|z) + c = ['x','y','z'].index(name[-1:]) + name = name[:-2] + if name not in inspect_gradients_list[opt_ctx.frame]: inspect_gradients_list[opt_ctx.frame][name] = np.asarray([0,0,0], dtype=np.float32) + inspect_gradients_list[opt_ctx.frame][name][c] = tf.reduce_mean(tf.abs(gradients)).numpy() + else: + abs_grad = tf.abs(gradients) + inspect_gradients_list[opt_ctx.frame][name] = [tf.reduce_mean(abs_grad).numpy(), tf.reduce_max(abs_grad).numpy()] + iig_func = None + if setup.training.density.pre_opt.inspect_gradients==2:# or True: + AABB_corners_WS = [] + for state in sequence: + dens_transform = state.get_density_transform() + if state.density.hull is not None: + AABB_corners_WS += dens_transform.transform_AABB(*hull_AABB_OS(tf.squeeze(state.density.hull, (0,-1))), True) + if state.density.hull is not None: + grad_cams = [main_camera.copy_clipped_to_world_coords(AABB_corners_WS)[0]] + else: + grad_cams = [main_camera] + ig_func = lambda opt_ctx, gradients, name: render_gradients(gradients, dens_transform, grad_cams, grad_renderer, \ + path=os.path.join(setup.paths.data, "gradients", "d_f{:04d}_it{:08d}".format(opt_ctx.frame, opt_ctx.iteration)), \ + image_mask=name.replace("/", ".") + "_b{batch:04d}_cam{view:02d}_{idx:04d}", name=name, log=log) + iig_func = lambda opt_ctx, gradients, name: write_image_gradients(gradients, max_renderer, \ + path=os.path.join(setup.paths.data, "gradients", "d_f{:04d}_it{:08d}".format(opt_ctx.frame, opt_ctx.iteration)), \ + image_mask=name.replace("/", ".") + "_img_cam{:04}", image_neg_mask=name.replace("/", ".") + "_img-neg_cam{:04}") + last_time = time.time() + start_time = last_time + + warp_sequence = False + fwd_warp_dens_clamp = setup.training.density.warp_clamp#'NONE' + warp_sequence_schedule = make_schedule(setup.training.density.main_warp_fwd) + + ### main opt loop ### + randomize_sequence_length = setup.training.randomization.sequence_length + if randomize_sequence_length: + log.info("Randomizing sequence length during training.") + last_max_sequence_length = len(sequence) + def get_train_sequence(sequence, iteration): + global last_max_sequence_length + min_length = 2 + max_length = int(loss_schedules.sequence_length(iteration)) + if max_length<1 or max_length>len(sequence): + max_length = len(sequence) + if not max_length==last_max_sequence_length: + log.info("Setting max sequence length from %d to %d in iteration %d", last_max_sequence_length, max_length, iteration) + last_max_sequence_length = max_length + + if randomize_sequence_length and max_length>min_length and not iteration==(setup.training.iterations-1): + length = np.random.randint(min_length, max_length+1) + log.debug("Iteration %d: sequence length = %d [%d,%d]", iteration, length, min_length, max_length) + else: + length = max_length + s = sequence.get_sub_sequence(length) + + return s + + # batch size limits + class BatchSizeHandler: + def __init__(self, base_bs, base_grp, max_grp=None, max_res=60, verbose=False): + self._base_bs = base_bs + self._curr_bs = base_bs + self._base_grp = base_grp + self._max_grp = max_grp + self._curr_grp = abs(base_grp) + self._max_res = max_res + self._verbose = verbose + def get(self): + return self._curr_bs, self._curr_grp + def _get_max_batch_size(self, res): + #return max(1, int(math.floor((self._max_res/res)**3))) + # 10GB GPU-mem, deep resblock models, MS sf 1.4, min res 10 + if res>48: return 1 #48 if sf 2.0, maybe 40 if sf is 1.4 + if res>34: return 2 + if res>24: return 4 + if res>18: return 8 + return 16 + def scale_to(self, res): + if self._base_grp<0: # override adaptive batch size by using a negative group size + new_bs = self._base_bs + new_grp = abs(self._base_grp) + else: + max_bs = self._get_max_batch_size(res) + if max_bs0: # level 0 has no skip weight + lifting_network_model.set_skip_merge_weight(grow_lifting_skip_schedules[level](it), level) + grow_lifting_lr[level].assign(grow_lifting_lr_schedules[level](it)) + if summary_iteration: + log.info("Grow lifting, it %d, level %d: skip %f, lr %f (%f), train %s, active %s", it, level, grow_lifting_skip_schedules[level](it), grow_lifting_lr_schedules[level](it), grow_lifting_lr[level].numpy(), grow_lifting_train_schedules[level](it), level<=lifting_network_model.get_active_level()) + + if grow_lifting_fade_residual: + # set output residual weights + for level in range(lifting_network_model.num_levels): + if level>0: # level 0 has no skip weight + lifting_network_model.set_output_residual_weight(grow_lifting_residual_schedules[level](it), level) + if summary_iteration: + log.info("Grow lifting, it %d, level %d: residual %f (%f), active %s", it, level, grow_lifting_residual_schedules[level](it), lifting_network_model.get_output_residual_weight(level), level<=lifting_network_model.get_active_level()) + + if grow_volenc_fade_residual: + # set output residual weights + for level in range(volume_encoder_model.num_levels): + if level>0: # level 0 has no skip weight + volume_encoder_model.set_output_residual_weight(grow_volenc_residual_schedules[level](it), level) + if summary_iteration: + log.info("Grow volenc, it %d, level %d: residual %f (%f), active %s", it, level, grow_volenc_residual_schedules[level](it), volume_encoder_model.get_output_residual_weight(level), level<=volume_encoder_model.get_active_level()) + + if grow_vel_MS_residual: + # set output residual weights + for level in range(len(grow_vel_MS_residual_schedules)): + # here level 0 can use residual weight + grow_vel_MS_residual_weight = grow_vel_MS_residual_schedules[level](it) + for state in sequence: + state.velocity.set_residual_weight(level, grow_vel_MS_residual_weight) + if summary_iteration: + log.info("Grow vel MS, it %d, level %d: residual %f %s, active %s", it, level, grow_vel_MS_residual_weight, \ + [state.velocity.get_residual_weight(level) for state in sequence], [level<=state.velocity.recursive_MS_current_level for state in sequence]) + + disc_imgs = [] + for batch_group in range(curr_batch_group_size): + with profiler.sample('Grad Step'): + summary_iteration = (batch_group==(curr_batch_group_size-1)) and ((it+1)%out_step==0 or (it+1)==setup.training.iterations or stop_training) + + if batch_variable_resolution: + if it==(setup.training.iterations-1) and (batch_group==(curr_batch_group_size-1)): #last iteration + grow_handler.is_randomize_shape = False + grow_handler.is_iterate_shapes = False + grow_handler.start_iteration(iteration=it) + log.debug("GrowHandler shapes: density:%s, camera:%s, velocity:%s, vel-level:%s", \ + grow_handler.get_density_shape(), grow_handler.get_camera_shape(), grow_handler.get_velocity_shape(), grow_handler.get_velocity_MS_scale()) + + if batch_variable_resolution or batch_group==0: + with profiler.sample('Set resolutions'): + v_scaled = scale_velocity(sequence, it, setup.training.velocity.grow.factor, setup.training.velocity.grow.scale_magnitude, setup.training.velocity.grow.intervals, base_shape=base_shape, verbose=not batch_variable_resolution) + d_scaled = scale_density(sequence, it, setup.training.density.grow.factor, setup.training.density.grow.intervals, base_shape=base_shape, save=False, \ + scale_all=((it==0) or (setup.training.randomization.grow_mode=="RAND") or v_scaled), verbose=not batch_variable_resolution) + grow_velocity_networks(sequence, it) + set_density_network_level(sequence) + scale_state_networks(sequence) + + sequence.clear_cache() + if setup.data.randomize>0: + with profiler.sample('Load targets'): + target_dataset.step() + sequence_set_targets(sequence, {state.frame: frame_loadTargets(setup, state.frame, sim_transform, target_dataset) for state in sequence}, \ + set_size=True) # also clears the cache #(setup.training.randomization.grid_size_relative==1) + sequence_randomize(sequence, randomize_input_views=setup.training.randomization.inputs, \ + randomize_target_views=setup.training.randomization.targets, \ + disable_transform_reset=True) + #randomize_transform=setup.training.randomization.transform) + + + if setup.training.randomization.grid_size_relative!=1: + with profiler.sample('Randomize scale'): + randomize_scale(sequence, min_size_abs=setup.training.randomization.grid_size_min, min_size_rel=setup.training.randomization.grid_size_relative, \ + max_shape=curr_vel_shape, train_cam_res=curr_cam_res, disc_cam_res=None) + + + + if warp_sequence_schedule(it)!=warp_sequence: + warp_sequence = warp_sequence_schedule(it) + if warp_sequence: + log.info("Density set to forward warped with clamp '%s' in iteration %d.", fwd_warp_dens_clamp, it) # + log.info("dens shapes %s, vel shapes %s", [_.density.shape for _ in sequence], [_.velocity.centered_shape for _ in sequence]) + if setup.training.density.decoder.active: + log.info("Set sequence densities for neural globt.") + sequence.set_density_for_neural_globt(order=opt_ctx.warp_order, dt=opt_ctx.dt, clamp=fwd_warp_dens_clamp, as_var=False, device=resource_device) + ##sequence.densities_advect_fwd(order=opt_ctx.warp_order, dt=opt_ctx.dt, clamp=fwd_warp_dens_clamp) + else: + log.info("Density set to per-frame in iteration %d.", it) # + elif warp_sequence and setup.data.randomize>0: + log.debug("Warp density forward with clamp '%s' in iteration %d.", fwd_warp_dens_clamp, it) #for testing, remove + #sequence.densities_advect_fwd(order=opt_ctx.warp_order, dt=opt_ctx.dt, clamp=fwd_warp_dens_clamp) + + #sequence.clear_cache() + + opt_ctx.start_iteration(it, compute_loss_summary=summary_iteration) + + seq_vel_add_noise(opt_ctx, sequence) + + train_sequence = get_train_sequence(sequence, it) + + if summary_iteration and setup.training.density.pre_opt.inspect_gradients:# or it==99 or it==200 or it==500: + for g in inspect_gradients_list: inspect_gradients_list[g].clear() + opt_ctx.set_inspect_gradient(True, ig_func, iig_func) + if setup.training.frame_order=='FWD-BWD': + loss_summaries = optStep_sequence(opt_ctx, train_sequence, disc_ctx, disc_samples_list=disc_imgs, order='FWD' if (it%2)==0 else 'BWD') + elif setup.training.frame_order=='BWD-FWD': + loss_summaries = optStep_sequence(opt_ctx, train_sequence, disc_ctx, disc_samples_list=disc_imgs, order='BWD' if (it%2)==0 else 'FWD') + else: + loss_summaries = optStep_sequence(opt_ctx, train_sequence, disc_ctx, disc_samples_list=disc_imgs, order=setup.training.frame_order) + + # Backprop + for state in train_sequence: + # only backprop if gradients are needed. + if state.requires_backprop: + #log.info("backprop state %d in it %d.", state.frame, it) + state._compute_input_grads() + if state.has_density_neural and state.density.requires_backprop: + #log.info("backprop neural density %d in it %d.", state.frame, it) + state.density._compute_input_grads() + if state.has_density_proxy and state.density_proxy.requires_backprop: + #log.info("backprop density proxy %d in it %d.", state.frame, it) + state.density_proxy._compute_input_grads() + if state.has_velocity and state.velocity.requires_backprop: + #log.info("backprop velocity %d in it %d.", state.frame, it) + state.velocity._compute_input_grads() + + with profiler.sample("regularization"): + print_regularization_stats = setup.debug.print_weight_grad_stats #True #True + add_reg_gradients = True + regularization_stats = {} if summary_iteration and print_regularization_stats else None + + regualrization_gradient_summary(model=target_encoder_model, weight=loss_schedules.density_decoder_regularization(it), apply=add_reg_gradients, \ + stats_dict=regularization_stats, name="View Encoder") + regualrization_gradient_summary(model=lifting_network_model, weight=loss_schedules.density_decoder_regularization(it), apply=add_reg_gradients, \ + stats_dict=regularization_stats, name="Lifting Network") + regualrization_gradient_summary(model=volume_encoder_model, weight=loss_schedules.density_decoder_regularization(it), apply=add_reg_gradients, \ + stats_dict=regularization_stats, name="Volume Encoder") + regualrization_gradient_summary(model=frame_merge_network_model, weight=loss_schedules.density_decoder_regularization(it), apply=add_reg_gradients, \ + stats_dict=regularization_stats, name="Frame Merge") + regualrization_gradient_summary(model=density_decoder_model, weight=loss_schedules.density_decoder_regularization(it), apply=add_reg_gradients, \ + stats_dict=regularization_stats, name="Density") + regualrization_gradient_summary(model=velocity_decoder_model, weight=loss_schedules.velocity_decoder_regularization(it), apply=add_reg_gradients, \ + stats_dict=regularization_stats, name="Velocity") + regualrization_gradient_summary(model=vel_input_encoder_model, weight=loss_schedules.velocity_decoder_regularization(it), apply=add_reg_gradients, \ + stats_dict=regularization_stats, name="VelocityInEnc") + regualrization_gradient_summary(model=vel_downscale_encoder_model, weight=loss_schedules.velocity_decoder_regularization(it), apply=add_reg_gradients, \ + stats_dict=regularization_stats, name="VelocityDownEnc") + + + if setup.debug.target_dump_samples: + log.warning("Dumping input/target images of iteration %d, batch %d.", it, batch_group) + dump_inputs(sequence, total_batch_idx) + total_batch_idx +=1 + # END profiling + # END batch group + + if summary_iteration: + for state in sequence: + opt_ctx.frame = state.frame + if state.has_velocity: + state.velocity.inspect_output_gradient_stats(opt_ctx) + opt_ctx.set_inspect_gradient(False) + + #if (it+1)%gradient_smoothing_window==0: + with profiler.sample('Apply gradients'): + + grads_vars_growing = [] + if grow_lifting_fade_layers: + grads_vars_growing = lifting_network_model.get_grads_vars_by_level(keep_gradients=False) + + grads_vars = [] + grads_vars_vel = [] + for state in sequence: + grads_vars.extend(state.get_grads_vars( \ + get_density_gradients=setup.training.density.decoder.active and isinstance(state.density, NeuralDensityGrid), \ + get_velocity_gradients=False, #setup.training.velocity.decoder.active and isinstance(state.velocity, NeuralVelocityGrid), + keep_gradients=False)) + + if isinstance(state.velocity, NeuralVelocityGrid): + grads_vars_vel.extend(state.velocity.get_grads_vars(keep_gradients=False, normalize=randomize_sequence_length)) + + DEBUG_check_duplicate_vars(grads_vars + grads_vars_vel) + + # need to normalize with smoothing window to be consistent with batch size normalization + # might not be needed with Adam, if batch_group_size stays constant + if not curr_batch_group_size==1: + grads_vars_growing = [scale_grads_for_smoothing(gvs, curr_batch_group_size) for gvs in grads_vars_growing] + grads_vars = scale_grads_for_smoothing(grads_vars, curr_batch_group_size) + grads_vars_vel = scale_grads_for_smoothing(grads_vars_vel, curr_batch_group_size) + + if grads_vars_growing: + for level, gvs in enumerate(grads_vars_growing): + if grow_lifting_train_schedules[level](it): + grow_lifting_optimizers[level].apply_gradients(gvs) + del grads_vars_growing + if grads_vars: + opt_ctx.density_optimizer.apply_gradients(grads_vars) + del grads_vars + if grads_vars_vel: + opt_ctx.velocity_optimizer.apply_gradients(grads_vars_vel) + del grads_vars_vel + + for state in sequence: + # just to be sure + # clears e.g. single-frame merge network regualrization gradients that never get applied. + state.clear_gradients(clear_density_gradients=True, clear_velocity_gradients=True) + + + del train_sequence + #if randomize_sequence_length: + sequence.restore_connections() + + # DISCRIMINATOR + disc_ctx.start_iteration(it, compute_loss_summary=summary_iteration) + disc_ctx.opt_ctx.frame = None + for disc_step in range(setup.training.discriminator.steps): + disc_loss = optStep_discriminator(disc_ctx, state=None, additional_fake_samples=disc_imgs) #_real, disc_loss_fake, disc_scores_real, disc_scores_fake + #END disc training + del disc_imgs + + + if args.console: # and not args.debug: + progress = it%out_step+1 + progress_bar(progress,out_step, "{:04d}/{:04d}".format(progress, out_step), length=50) + + if summary_iteration: + log.info('--- Step {:04d}/{:04d} ---'.format(it, setup.training.iterations-1)) + print_loss_summary(loss_summaries, [], start_time, last_time, it, setup.training.iterations, \ + inspect_gradients_list if setup.training.density.pre_opt.inspect_gradients==1 else None, regularization_stats=regularization_stats) + regularization_stats = None + log.info("buoyancy: %s", opt_ctx.buoyancy.numpy()) + #d_max, d_min, d_mean = tf_print_stats(state.density.d, 'Density', log=log) + if setup.training.light.optimize: + log.info("Light intensities: %s", [_.numpy() for _ in light_var_list]) + if disc_ctx is not None and disc_ctx.train and setup.training.discriminator.start_delay<=it: + print_disc_summary(disc_ctx, disc_loss)#_real, disc_scores_real, disc_loss_fake, disc_scores_fake) + last_time = time.time() + summary_writer.flush() + try: + check_loss_summary(loss_summaries, [], it, inspect_gradients_list if setup.training.density.pre_opt.inspect_gradients==1 else None, grad_max=5e-2) + except ValueError as e: + log.exception("Invalid loss summary in iteration %d:", it) + dump_inputs(sequence, it) + dump_vel_input_features(sequence, it) + stop_training = True + + if val_cameras is not None and (it+1)%val_out_step==0: + sequence.clear_cache() + if setup.data.randomize>0: + sequence_set_targets(sequence, val_sequence) + sequence_randomize(sequence, randomize_input_views=setup.validation.input_view_mask, disable_transform_reset=True) #reset randomization + log.info("Render validation views %d in iteration %d", int((it+1)//val_out_step), it) + #sequence.clear_cache() + try: + render_sequence_val(sequence, z, int((it+1)//val_out_step)) + except: + log.exception('Exception when rendering validation views %d for sequence in iteration %d:', int((it+1)//val_out_step), it) + #sequence.clear_cache() + + + if setup.training.checkpoint_interval>0 and (it+1)%setup.training.checkpoint_interval==0: + try: + save_checkpoint() + except: + log.exception("Exception when saving checkpoint in iteration %d:", it+1) + + if psutil.Process(os.getpid()).memory_info().rss>max_memory: + log.error("Current memory exceeds limit, stopping.") + stop_training = True + + if stop_training: + log.warning('Training stopped after %d iterations, saving state...', it+1) + raise StopTraining + break + #iteration profiler + #END for it in iterations (training loop) + #optimization profiler + #tf summary + log.debug('Save sequence') + sequence.save() + if setup.training.discriminator.active: + save_NNmodel(disc_ctx.model, 'disc', setup.paths.data) + if setup.training.discriminator.history.save: + disc_ctx.history.serialize(setup.paths.data) + if (setup.training.density.decoder.active or setup.training.velocity.decoder.active) and target_encoder_model is not None: + save_NNmodel(target_encoder_model, 'target_encoder', setup.paths.data) + if lifting_network_model is not None: + save_NNmodel(lifting_network_model, 'lifting_network', setup.paths.data) + if setup.training.volume_encoder.active and volume_encoder_model is not None: + save_NNmodel(volume_encoder_model, 'volume_encoder', setup.paths.data) + if setup.training.frame_merge_network.active and frame_merge_network_model is not None: + save_NNmodel(frame_merge_network_model, 'frame_merge_network', setup.paths.data) + if setup.training.density.decoder.active and density_decoder_model is not None: + save_NNmodel(density_decoder_model, 'density_decoder', setup.paths.data) + if setup.training.velocity.decoder.active and velocity_decoder_model is not None: + save_NNmodel(velocity_decoder_model, 'velocity_decoder', setup.paths.data) + if setup.training.velocity.decoder.active and vel_input_encoder_model is not None: + save_NNmodel(vel_input_encoder_model, 'velocity_input_encoder', setup.paths.data) + if setup.training.velocity.decoder.active and vel_downscale_encoder_model is not None: + save_NNmodel(vel_downscale_encoder_model, 'velocity_downscale_encoder', setup.paths.data) + + + except StopTraining: + log.warning('Optimization stopped after %s, saving state...', format_time(time.time() - optim_start)) + log.debug('Save sequence') + sequence.save(suffix="part") + if setup.training.discriminator.active: + save_NNmodel(disc_ctx.model, 'disc_part', setup.paths.data) + if setup.training.discriminator.history.save: + disc_ctx.history.serialize(setup.paths.data, 'part') + if (setup.training.density.decoder.active or setup.training.velocity.decoder.active) and target_encoder_model is not None: + save_NNmodel(target_encoder_model, 'target_encoder_part', setup.paths.data) + if lifting_network_model is not None: + save_NNmodel(lifting_network_model, 'lifting_network_part', setup.paths.data) + if setup.training.volume_encoder.active and volume_encoder_model is not None: + save_NNmodel(volume_encoder_model, 'volume_encoder_part', setup.paths.data) + if setup.training.frame_merge_network.active and frame_merge_network_model is not None: + save_NNmodel(frame_merge_network_model, 'frame_merge_network_part', setup.paths.data) + if setup.training.density.decoder.active and density_decoder_model is not None: + save_NNmodel(density_decoder_model, 'density_decoder_part', setup.paths.data) + if setup.training.velocity.decoder.active and velocity_decoder_model is not None: + save_NNmodel(velocity_decoder_model, 'velocity_decoder_part', setup.paths.data) + if setup.training.velocity.decoder.active and vel_input_encoder_model is not None: + save_NNmodel(vel_input_encoder_model, 'velocity_input_encoder_part', setup.paths.data) + if setup.training.velocity.decoder.active and vel_downscale_encoder_model is not None: + save_NNmodel(vel_downscale_encoder_model, 'velocity_downscale_encoder_part', setup.paths.data) + + # something unexpected happended. save state if possible and exit. + except: + log.exception('Exception during training. Attempting to save state...') + try: + summary_writer.close() + except: + log.error('Could not close summary writer', exc_info=True) + if 'sequence' in locals(): + try: + sequence.save(suffix="exc") + except: + log.error('Could not save sequence', exc_info=True) + + if 'disc_model' in locals(): + try: + save_NNmodel(disc_ctx.model, 'disc_exc', setup.paths.data) + if setup.training.discriminator.history.save: + disc_ctx.history.serialize(setup.paths.data, 'exc') + except: + log.exception('Could not save discriminator') + if 'target_encoder_model' in locals() and target_encoder_model is not None: + try: + save_NNmodel(target_encoder_model, 'target_encoder_exc', setup.paths.data) + except: + log.exception('Could not save target encoder') + if 'lifting_network_model' in locals() and lifting_network_model is not None: + try: + save_NNmodel(lifting_network_model, 'lifting_network_exc', setup.paths.data) + except: + log.exception('Could not save lifting network') + if 'volume_encoder_model' in locals() and volume_encoder_model is not None: + try: + save_NNmodel(volume_encoder_model, 'volume_encoder_exc', setup.paths.data) + except: + log.exception('Could not save volume encoder') + if 'frame_merge_network_model' in locals() and frame_merge_network_model is not None: + try: + save_NNmodel(frame_merge_network_model, 'frame_merge_network_exc', setup.paths.data) + except: + log.exception('Could not save frame merge network') + if 'density_decoder_model' in locals(): + try: + save_NNmodel(density_decoder_model, 'density_decoder_exc', setup.paths.data) + except: + log.exception('Could not save density decoder') + if 'velocity_decoder_model' in locals(): + try: + save_NNmodel(velocity_decoder_model, 'velocity_decoder_exc', setup.paths.data) + except: + log.exception('Could not save velocity decoder') + if 'vel_input_encoder_model' in locals(): + try: + save_NNmodel(vel_input_encoder_model, 'velocity_input_encoder_exc', setup.paths.data) + except: + log.exception('Could not save velocity input encoder') + if 'vel_downscale_encoder_model' in locals(): + try: + save_NNmodel(vel_downscale_encoder_model, 'velocity_downscale_encoder_exc', setup.paths.data) + except: + log.exception('Could not save velocity downscale encoder') + try: + with open(os.path.join(setup.paths.log, 'profiling.txt'), 'w') as f: + profiler.stats(f) + except: + log.exception('Could not save profiling') + faulthandler.disable() + faultlog.close() + sys.exit(1) + else: + log.info('Optimization finished after %s', format_time(time.time() - optim_start)) + finally: + # reset signal handling + signal.signal(signal.SIGINT, signal.SIG_DFL) + + with open(os.path.join(setup.paths.log, 'profiling.txt'), 'w') as f: + profiler.stats(f) + faulthandler.disable() + faultlog.close() + + scalar_results = munch.Munch() + scalar_results.buoyancy = buoyancy.numpy().tolist() if setup.training.optimize_buoyancy else None + scalar_results.light_intensity = [_.numpy().tolist() for _ in light_var_list] if setup.training.light.optimize else None + final_transform = sim_transform.copy_no_data() + final_transform.grid_size = sequence[0].transform.grid_size if setup.training.density.decoder.active else sequence[0].density.shape + scalar_results.sim_transform = final_transform + + with open(os.path.join(setup.paths.data, "scalar_results.json"), "w") as f: + try: + json.dump(scalar_results, f, default=tf_to_dict, sort_keys=True, indent=2) + except: + log.exception("Failed to write scalar_results:") + + + if not args.fit: + log.debug('Load data') + if setup.data.load_sequence is None: + raise ValueError("No sequence specified (setup.data.load_sequence)") + sf = RunIndex.parse_scalarFlow(setup.data.load_sequence) + if sf is not None: + log.info("Load scalarFlow sequence for evaluation, sim offset %d, frame offset %d", sf["sim"], sf["frame"]) + frames = list(range(setup.data.start+sf["frame"], setup.data.stop+sf["frame"], setup.data.step)) + vel_bounds = None if setup.data.velocity.boundary.upper()=='CLAMP' else Zeroset(-1, shape=GridShape(), outer_bounds="CLOSED", as_var=False, device=resource_device) + with profiler.sample("load sF sequence"): + if args.console: + load_bar = ProgressBar(len(frames), name="Load Sequence: ") + def update_pbar(step, frame): + load_bar.update(step, desc="Frame {:03d} ({:03d}/{:03d})".format(frame, step+1, len(frames))) + else: update_pbar = lambda i, f: None + sequence = Sequence.from_scalarFlow_file(pFmt.format(setup.data.density.scalarFlow_reconstruction, sim=setup.data.simulation+sf["sim"]), \ + pFmt.format(setup.data.velocity.scalarFlow_reconstruction, sim=setup.data.simulation+sf["sim"]), \ + frames, transform=sim_transform, #sF_transform, + as_var=False, base_path=setup.paths.data, boundary=vel_bounds, scale_renderer=scale_renderer, warp_renderer=warp_renderer, device=resource_device, frame_callback=update_pbar) + if args.console: load_bar.finish(desc="Done") + frames = list(range(setup.data.start, setup.data.stop, setup.data.step)) + for s,f in zip(sequence, frames): + s.base_target_cameras = setup_target_cameras(target_cameras, train_cam_resolution, None, setup.rendering.target_cameras.crop_frustum_pad) + s.velocity.scale_magnitude(setup.data.step) + s.density.scale(setup.data.density.scale) + s.frame = f + else: + load_entry = run_index.get_run_entry(setup.data.load_sequence) + log.info("Load sequence from '%s' for evaluation", load_entry.path) + try: + load_setup = munch.munchify(load_entry.setup) + vel_bounds = None if setup.data.velocity.boundary.upper()=='CLAMP' else Zeroset(-1, shape=GridShape(), outer_bounds="CLOSED", as_var=False, device=resource_device) + try: + vel_bounds = None if load_setup.data.velocity.boundary.upper()=='CLAMP' else Zeroset(-1, shape=GridShape(), outer_bounds="CLOSED", as_var=False, device=resource_device) + except: + log.info("Using default boundaries: %s", vel_bounds) + except: + log.exception("failed to load config from %s:", load_entry.path) + sys.exit(1) + frames = list(range(setup.data.start, setup.data.stop, setup.data.step)) + + if setup.data.step!=load_setup.data.step: + log.info("Loaded frame step does not match data, scaling velocity with %f", setup.data.step/load_setup.data.step) + + try: + load_scalars = load_entry.scalars + t = from_dict(load_scalars["sim_transform"]) + except: + log.exception("Failed to load transformation, using default.") + else: + sim_transform = t + sim_transform.grid_size = density_size.as_shape + log.info("current grid transformation: %s", sim_transform) + if setup.validation.synth_data_eval_setup.upper()=="SF": # and False: + sim_transform.parent.parent.translation[1]=sim_transform.grid_size_world().y/2 + 0.005 + log.info("modified grid transformation: %s", sim_transform) + + + + log.info("-> cell size world: %s", sim_transform.cell_size_world()) + ### EVALUATION SETUP + + # -- Load Dataset -- + + load_density_dataset = ("TARGET" in setup.validation.warp_test) or (setup.validation.stats and setup.validation.cmp_vol_targets) or args.save_volume #or (not setup.training.density.decoder.active) #True + load_velocity_dataset = False #("TARGET" in setup.validation.warp_test) or (setup.validation.stats and setup.validation.cmp_vol_targets) #or (not setup.training.velocity.decoder.active) #True + sequence_length = len(frames) #setup.data.sequence_length + sequence_step = setup.data.step #setup.data.sequence_step + + def get_max_cell_size(): + min_grid_res = [setup.training.velocity.decoder.min_grid_res, int(math.ceil(setup.training.velocity.decoder.min_grid_res * setup.data.y_scale)), setup.training.velocity.decoder.min_grid_res] + tmp_T = sim_transform.copy_no_data() + tmp_T.grid_size = min_grid_res + max_cell_size = tmp_T.cell_size_world() + return min(max_cell_size) + synth_max_cell_size = get_max_cell_size() #cell size at coarsest resolution, as defined by min_grid_res + synth_max_translation = synth_max_cell_size * setup.data.synth_shapes.max_translation #0.08 + + eval_data = setup.validation.synth_data_eval_setup.upper() #"CUBE" # SF, SPHERE, CUBE, ROTCUBE, STATICCUBE + eval_data_is_dataset = [] + if eval_data in ["SF", "SF_RENDER"]: + sequence_length_frames = sequence_length*sequence_step + + eval_data_is_dataset += ["SF", "SF_RENDER"] + def resolve_paths(): + path_raw = run_index[setup.data.density.target] + if path_raw is None: + path_raw = setup.data.density.target + else: + pass #raise NotImplementedError + path_preproc = None + + path_density = run_index[setup.data.density.initial_value] + if path_density is None: #SF data + path_density = setup.data.density.initial_value + dens_src_transform = sF_transform + dens_type = "SF" + else: # GlobTrans reconstruction + dens_entry = run_index.get_run_entry(setup.data.density.initial_value) + dens_src_transform = from_dict(dens_entry.scalars["sim_transform"]) + dens_type = "OWN" + + path_velocity = run_index[setup.data.velocity.initial_value] + if path_velocity is None: #SF data + path_velocity = setup.data.velocity.initial_value + vel_src_transform = sF_transform + vel_type = "SF" + else: # GlobTrans reconstruction + vel_entry = run_index.get_run_entry(setup.data.velocity.initial_value) + vel_src_transform = from_dict(vel_entry.scalars["sim_transform"]) + vel_type = "OWN" + + return {"path_raw":path_raw, "path_preproc":path_preproc, + "path_density":path_density, "density_t_src":dens_src_transform, "density_type":dens_type, + "path_velocity":path_velocity, "velocity_t_src":vel_src_transform, "velocity_type":vel_type, } + + kwargs = {} + if eval_data=="SF_RENDER": + load_density_dataset = True + kwargs["render_targets"] = True + kwargs["density_renderer"] = synth_target_renderer + kwargs["cameras"] = target_cameras + kwargs["lights"] = lights + + target_dataset, target_data_cache = get_targets_dataset_v2(sim_indices=setup.data.sims, frame_start=setup.data.start, frame_stop=setup.data.start+sequence_length_frames, frame_strides=sequence_length_frames, \ + raw=True, preproc=True, bkg=True, hull=True, batch_size=1, \ + sequence_step=sequence_step, sequence_length=sequence_length, \ + view_indices=[scalarFlow_cam_ids[_] for _ in setup.data.density.target_cam_ids], num_views=len(setup.data.density.target_cam_ids), + SF_frame_offset=setup.data.scalarFlow_frame_offset, \ + down_scale=setup.training.train_res_down, channels=color_channel, threshold=setup.data.density.hull_threshold, shuffle_frames=False,\ + density=load_density_dataset, density_t_dst=sim_transform, density_sampler=scale_renderer, \ + velocity=load_velocity_dataset, \ + cache_device=data_device, randomize_transform=False, \ + **resolve_paths(), **kwargs) + #path_raw=setup.data.density.target, path_preproc=None, path_density=setup.data.density.initial_value, density_t_src=sF_transform, path_velocity=setup.data.velocity.initial_value) + #dataset_size = len(setup.data.sims) * int(np.ceil((setup.data.stop - setup.data.start)/setup.data.step)) #//batch_size + elif eval_data == "INFLOW_TEST": + eval_data_is_dataset += ["INFLOW_TEST"] + sequence_length_frames = sequence_length*sequence_step + + target_dataset, target_data_cache = get_targets_dataset_v2(sim_indices=setup.data.sims, frame_start=setup.data.start, frame_stop=setup.data.start+sequence_length_frames, frame_strides=sequence_length_frames, \ + raw=True, preproc=True, bkg=True, hull=True, batch_size=1, \ + sequence_step=sequence_step, sequence_length=sequence_length, \ + view_indices=[scalarFlow_cam_ids[_] for _ in setup.data.density.target_cam_ids], num_views=len(setup.data.density.target_cam_ids), + SF_frame_offset=setup.data.scalarFlow_frame_offset, \ + down_scale=setup.training.train_res_down, channels=color_channel, threshold=setup.data.density.hull_threshold, shuffle_frames=False,\ + density=load_density_dataset, density_t_dst=sim_transform, density_sampler=scale_renderer, \ + velocity=load_velocity_dataset, \ + cache_device=data_device, randomize_transform=False, \ + render_targets=True, density_renderer=synth_target_renderer, cameras=target_cameras, lights=lights, \ + path_density="/home/franz/data/fluid_sim/manta/synth_plume_128_{sim:06d}/density_{frame:06d}.npz", \ + density_t_src=sF_transform, velocity_t_src=sF_transform, density_type="MANTA", \ + path_raw=None) + elif eval_data in ["SPHERE", "CUBE", "TORUS"]: + log.info("Using synthetic dataset %s for evaluation", eval_data) + + eval_data_is_dataset += ["SPHERE", "CUBE", "TORUS"] + if not setup.data.SDF: + target_dataset = get_synthTargets_dataset_v2(batch_size=1, base_grid_transform=sim_transform, sequence_length=sequence_length, \ + view_indices=[scalarFlow_cam_ids[_] for _ in setup.data.density.target_cam_ids], num_views=len(setup.data.density.target_cam_ids), \ + cameras=target_cameras, lights=lights, device=resource_device, \ + density_range=[0.15,0.25], inner_range=[0.1,0.4], scale_range=[0.3,0.5], \ + translation_range=[-synth_max_translation,synth_max_translation], \ + rotation_range=[10,30], \ + raw=True, preproc=True, bkg=True, hull=True, mask=setup.data.SDF, channels=1, SDF=setup.data.SDF, \ + density=load_density_dataset, velocity=load_velocity_dataset, advect_density=False, density_sampler=density_sampler, density_renderer=synth_target_renderer, \ + seed=np.random.randint(np.iinfo(np.int32).max) if setup.validation.synth_data_seed is None else setup.validation.synth_data_seed, \ + sample_overrides={"shape_type":(5 if eval_data=="TORUS" else (0 if eval_data=="SPHERE" else 1)), }) #'density_scale':0.1, "base_scale":[0.18]*3, "initial_translation":[0,0,0], + else: + target_dataset = get_synthTargets_dataset_v2(batch_size=1, base_grid_transform=sim_transform, sequence_length=sequence_length, \ + view_indices=[scalarFlow_cam_ids[_] for _ in setup.data.density.target_cam_ids], num_views=len(setup.data.density.target_cam_ids), \ + cameras=target_cameras, lights=lights, device=resource_device, \ + density_range=[0.05,0.14], inner_range=[0.1,0.4], scale_range=[0.3,0.5], translation_range=[-synth_max_translation,synth_max_translation], rotation_range=[0,20], \ + raw=True, preproc=True, bkg=True, hull=True, mask=setup.data.SDF, channels=1, SDF=setup.data.SDF, \ + density=load_density_dataset, velocity=load_velocity_dataset, advect_density=not setup.data.SDF, density_sampler=density_sampler, density_renderer=synth_target_renderer, \ + seed=np.random.randint(np.iinfo(np.int32).max) if setup.validation.synth_data_seed is None else setup.validation.synth_data_seed, \ + sample_overrides={'density_scale':0.1, "base_scale":[0.30]*3, "shape_type":(0 if eval_data=="SPHERE" else 1), "initial_translation":[0,0,0], }) + elif eval_data=="ROTCUBE": + log.info("Using synthetic dataset %s for evaluation", eval_data) + + eval_data_is_dataset += ["ROTCUBE"] + target_dataset = get_synthTargets_dataset_v2(batch_size=1, base_grid_transform=sim_transform, sequence_length=sequence_length, \ + view_indices=[scalarFlow_cam_ids[_] for _ in setup.data.density.target_cam_ids], num_views=len(setup.data.density.target_cam_ids), \ + cameras=target_cameras, lights=lights, device=resource_device, \ + density_range=[0.05,0.14], inner_range=[0.1,0.4], scale_range=[0.14,0.22], translation_range=[0,0], rotation_range=[0,20], \ + raw=True, preproc=True, bkg=True, hull=True, channels=1, mask=setup.data.SDF, SDF=setup.data.SDF, \ + density=load_density_dataset, velocity=load_velocity_dataset, density_sampler=density_sampler, density_renderer=synth_target_renderer, randomize_transform=False, \ + seed=np.random.randint(np.iinfo(np.int32).max) if setup.validation.synth_data_seed is None else setup.validation.synth_data_seed, \ + sample_overrides={'density_scale':0.1, "base_scale":[0.16,0.4,0.24], "shape_type":1, "initial_translation":[0,0,0], "initial_rotation_rotvec":[0,0,0], "rotvec":[0,0,0.17]}) + # + elif eval_data in ["STATICCUBE"]: + log.info("Using synthetic dataset %s for evaluation", eval_data) + + eval_data_is_dataset += ["STATICCUBE"] + target_dataset = get_synthTargets_dataset_v2(batch_size=1, base_grid_transform=sim_transform, sequence_length=sequence_length, \ + view_indices=[scalarFlow_cam_ids[_] for _ in setup.data.density.target_cam_ids], num_views=len(setup.data.density.target_cam_ids), \ + cameras=target_cameras, lights=lights, device=resource_device, \ + density_range=[0.05,0.14], inner_range=[0.1,0.4], scale_range=[0.14,0.22], translation_range=[0,0], rotation_range=[0,0], \ + raw=True, preproc=True, bkg=True, hull=True, channels=1, mask=setup.data.SDF, SDF=setup.data.SDF, \ + density=load_density_dataset, velocity=load_velocity_dataset, density_sampler=density_sampler, density_renderer=synth_target_renderer, randomize_transform=False, \ + seed=np.random.randint(np.iinfo(np.int32).max) if setup.validation.synth_data_seed is None else setup.validation.synth_data_seed, \ + sample_overrides={'density_scale':0.1, "base_scale":[0.18]*3, "shape_type":1, "initial_translation":[0,0,0], "initial_rotation_rotvec":[0,0,0]}) + # returns: [NSVHWC for type] + else: + raise ValueError("Unknown eval data '%s'."%(eval_data,)) + if not eval_data in eval_data_is_dataset: + target_dataset = TargetDataset(target_data, resource_device=resource_device) + + + + def get_max_recursive_MS_grow_levels(decoder_config, cast_fn=round): + if decoder_config.recursive_MS_levels=="VARIABLE": + #return GrowingUNet.get_max_levels(sim_transform.grid_size, scale_factor=load_setup.training.velocity.decoder.model.level_scale_factor, min_size=setup.training.velocity.decoder.min_grid_res) + i = 0 + while (min(_/(decoder_config.recursive_MS_scale_factor**i) for _ in sim_transform.grid_size)>=decoder_config.min_grid_res): + i +=1 + return i + else: + return decoder_config.recursive_MS_levels + + with profiler.sample("load sequence"): + + # -- Setup and Load Networks -- + + target_encoder_model = None + if "NETWORK" in load_setup.training.view_encoder.encoder: # and setup.training.view_encoder.load_encoder is not None: + assert isinstance(setup.training.view_encoder.model, str) + model_path = run_index[setup.training.view_encoder.model] + if model_path is None: + model_path = setup.training.view_encoder.model + #target_encoder_model = tf.keras.models.load_model(model_path, custom_objects=custom_keras_objects) + target_encoder_model = load_model(setup.training.view_encoder.model, num_levels=None, input_merge_weight=0.5, skip_merge_weight=1.0) + string_buffer = StringWriter() + string_buffer.write_line("View Encoder Model:") + target_encoder_model.summary(print_fn=string_buffer.write_line) + log.info(string_buffer.get_string()) + string_buffer.reset() + + velocity_decoder_model = None + vel_input_encoder_model = None + vel_downscale_encoder_model = None + # TODO: recMS non-shared decoder loading + #velocity_grid = None + if isinstance(setup.training.velocity.decoder.model, str): + if load_setup.training.velocity.decoder.recursive_MS and not load_setup.training.velocity.decoder.recursive_MS_shared_model: + model_path = run_index[setup.training.velocity.decoder.model] #setup.training.density.decoder.model] + if model_path is None: + model_path = setup.training.velocity.decoder.model + model_paths = get_NNmodel_names(model_path) + max_levels = get_max_recursive_MS_grow_levels(setup.training.velocity.decoder) + if max_levels>len(model_paths): + log.error("%s only has %d levels, but %d were requested.", setup.training.velocity.decoder.model, len(model_paths), max_levels) + raise SystemExit(1) + log.info("load %d/%d decoders for non-shared recursive multi-scale velocity.", max_levels, len(model_paths)) + velocity_decoder_model = [] + if (not hasattr(load_setup.training.velocity.decoder.model, "num_levels")) or load_setup.training.velocity.decoder.model.num_levels=="VARIABLE": + num_levels = GrowingUNet.get_max_levels(sim_transform.grid_size, scale_factor=load_setup.training.velocity.decoder.recursive_MS_scale_factor, min_size=load_setup.training.velocity.decoder.min_grid_res) + else: + num_levels = load_setup.training.velocity.decoder.model.num_levels + for level in range(max_levels): + velocity_decoder_model.append(load_model(model_paths[level], num_levels=num_levels, input_merge_weight=0.5, skip_merge_weight=1.0)) + string_buffer = StringWriter() + string_buffer.write_line("Velocity Decoder Model %d:"%(level,)) + velocity_decoder_model[-1].summary(print_fn=string_buffer.write_line) + log.info(string_buffer.get_string()) + string_buffer.reset() + + #velocity_decoder_model.append(setup_velocity_decoder(velocity_decoder_input_channels, name="VelocityDecoder_L{:03d}".format(level))) + #raise NotImplementedError("recusive MS with multiple decoders not yet implemented.") + else: + max_levels = GrowingUNet.get_max_levels(sim_transform.grid_size, load_setup.training.velocity.decoder.recursive_MS_scale_factor, min_size=load_setup.training.density.decoder.min_grid_res) + velocity_decoder_model = load_model(setup.training.velocity.decoder.model, num_levels=max_levels, input_merge_weight=0.5, skip_merge_weight=1.0) + string_buffer = StringWriter() + string_buffer.write_line("Velocity Decoder Model:") + velocity_decoder_model.summary(print_fn=string_buffer.write_line) + log.info(string_buffer.get_string()) + string_buffer.reset() + elif load_setup.training.velocity.decoder.active: + log.warning("velocity decoder was active in training") + + # velocity_grid = NeuralVelocityGrid(volume_decoder=velocity_decoder_model, boundary=vel_bounds, scale_renderer=scale_renderer, warp_renderer=warp_renderer, device=resource_device, parent_state=None) + # velocity_grid.use_raw_images = load_setup.training.velocity.decoder.input_type=='RAW' + + density_decoder_model = None + #density_grid = None + if isinstance(setup.training.density.decoder.model, str): + if load_setup.training.density.decoder.recursive_MS and not load_setup.training.density.decoder.recursive_MS_shared_model: + model_path = run_index[setup.training.density.decoder.model] #setup.training.density.decoder.model] + if model_path is None: + model_path = setup.training.density.decoder.model + model_paths = get_NNmodel_names(model_path) + max_levels = get_max_recursive_MS_grow_levels(setup.training.density.decoder) + if max_levels>len(model_paths): + log.error("%s only has %d levels, but %d were requested.", setup.training.density.decoder.model, len(model_paths), max_levels) + raise SystemExit(1) + log.info("load %d/%d decoders for non-shared recursive multi-scale density.", max_levels, len(model_paths)) + density_decoder_model = [] + if (not hasattr(load_setup.training.density.decoder.model, "num_levels")) or load_setup.training.density.decoder.model.num_levels=="VARIABLE": + num_levels = GrowingUNet.get_max_levels(sim_transform.grid_size, scale_factor=load_setup.training.density.decoder.recursive_MS_scale_factor, min_size=load_setup.training.density.decoder.min_grid_res) + else: + num_levels = load_setup.training.density.decoder.model.num_levels + for level in range(max_levels): + density_decoder_model.append(load_model(model_paths[level], num_levels=num_levels, input_merge_weight=0.5, skip_merge_weight=1.0)) + string_buffer = StringWriter() + string_buffer.write_line("Density Decoder Model %d:"%(level,)) + density_decoder_model[-1].summary(print_fn=string_buffer.write_line) + log.info(string_buffer.get_string()) + string_buffer.reset() + + #density_decoder_model.append(setup_velocity_decoder(velocity_decoder_input_channels, name="VelocityDecoder_L{:03d}".format(level))) + #raise NotImplementedError("recusive MS with multiple decoders not yet implemented.") + else: + max_levels = GrowingUNet.get_max_levels(sim_transform.grid_size, load_setup.training.density.decoder.recursive_MS_scale_factor, min_size=load_setup.training.density.decoder.min_grid_res) + density_decoder_model = load_model(setup.training.density.decoder.model, num_levels=max_levels, input_merge_weight=0.5, skip_merge_weight=1.0) + string_buffer = StringWriter() + string_buffer.write_line("Density Decoder Model:") + density_decoder_model.summary(print_fn=string_buffer.write_line) + log.info(string_buffer.get_string()) + string_buffer.reset() + elif load_setup.training.density.decoder.active: + log.warning("density decoder was active in training") + + + volume_encoder_model = None + lifting_network_model = None + if setup.training.view_encoder.lifting.upper()=="UNPROJECT" and setup.training.volume_encoder.active and isinstance(setup.training.volume_encoder.model, str): + volume_encoder_model = load_model(setup.training.volume_encoder.model, num_levels=None, input_merge_weight=0.5, skip_merge_weight=1.0) + string_buffer = StringWriter() + string_buffer.write_line("Volume Encoder Model:") + volume_encoder_model.summary(print_fn=string_buffer.write_line) + log.info(string_buffer.get_string()) + string_buffer.reset() + elif load_setup.training.view_encoder.lifting.upper()=="UNPROJECT" and "volume_encoder" in load_setup.training and load_setup.training.volume_encoder.active: + log.warning("volume_encoder was active in training") + + if setup.training.view_encoder.lifting.upper()=="NETWORK" and setup.training.lifting_network.active and isinstance(setup.training.lifting_network.model, str): + #assert len(target_cameras)==26 + assert len(setup.validation.input_view_mask)==1 # and setup.validation.input_view_mask[0]==0 + max_levels = GrowingUNet.get_max_levels(sim_transform.grid_size, 2 if isinstance(load_setup.training.lifting_network.model, str) else load_setup.training.lifting_network.model.level_scale_factor, min_size=load_setup.training.lifting_network.min_grid_res) + lifting_cameras = [target_cameras[_] for _ in setup.validation.input_view_mask] + log.info("Number of levels for lifting network: %d", max_levels) + lifting_network_model = load_model(setup.training.lifting_network.model, num_levels=max_levels, skip_merge_weight=1.0, output_residual_weight=0.0, \ + lifting_renderer=lifting_renderer, lifting_cameras=lifting_cameras, lifting_transform=sim_transform.copy_no_data(), lifting_shape=sim_transform.grid_size) + #, input_merge_weight=0.5, skip_merge_weight=1.0 + string_buffer = StringWriter() + string_buffer.write_line("Lifting Network Model:") + lifting_network_model.summary(print_fn=string_buffer.write_line) + log.info(string_buffer.get_string()) + string_buffer.reset() + + # log.info("active level: %d", lifting_network_model.get_active_level()) + # lifting_network_model.set_active_level_from_grid_size(grid_size=lifting_cameras[0].transform.grid_size[1:], min_size=load_setup.training.lifting_network.min_grid_res, lifting_size=sim_transform.grid_size) + log.info("active levels: %d, output mode: %s", lifting_network_model.get_active_level()+1, lifting_network_model.output_mode) + if lifting_network_model.output_mode=="RESIDUAL_WEIGHTED": + log.info("residual output weights: %s", [lifting_network_model.get_output_residual_weight(l) for l in range(1, lifting_network_model.get_active_level()+1)]) + + elif load_setup.training.view_encoder.lifting.upper()=="NETWORK" and "lifting_network" in load_setup.training and load_setup.training.lifting_network.active: + log.warning("lifting_network was active in training") + + frame_merge_network_model = None + if setup.training.frame_merge_network.active and isinstance(setup.training.frame_merge_network.model, str): + frame_merge_network_model = load_model(setup.training.frame_merge_network.model, num_levels=None, input_merge_weight=0.5, skip_merge_weight=1.0) + string_buffer = StringWriter() + string_buffer.write_line("Frame Merge Model:") + frame_merge_network_model.summary(print_fn=string_buffer.write_line) + log.info(string_buffer.get_string()) + string_buffer.reset() + elif "frame_merge_network" in load_setup.training and load_setup.training.frame_merge_network.active: + log.warning("frame_merge_network was active in training") + + def frame_velTargetSetup(aux_sequence, frame): + if not ("velocity" in aux_sequence[frame]) and (aux_sequence[frame].velocity is not None): + raise ValueError("") + vel_var_name = "velocityTarget_f{:06d}".format(frame) + velocity = VelocityGrid.from_staggered_combined(aux_sequence[frame].velocity, as_var=False, boundary=vel_bounds, \ + scale_renderer=scale_renderer, warp_renderer=warp_renderer, device=resource_device, var_name=vel_var_name, trainable=False) + return velocity + def frame_densTargetSetup(aux_sequence, frame): + if not ("density" in aux_sequence[frame]) and (aux_sequence[frame].density is not None): + raise ValueError("") + density_grid_shape = GridShape.from_tensor(aux_sequence[frame].density).spatial_vector.as_shape + density = DensityGrid(density_grid_shape, d=aux_sequence[frame].density, as_var=False, \ + scale_renderer=scale_renderer, hull=None, inflow=None, inflow_offset=None, inflow_mask=None, \ + device=resource_device, restrict_to_hull=setup.training.density.use_hull, is_SDF=setup.data.SDF) + return density + + # -- Setup Data Modules -- + + def build_sequence(frames, dataset): + sequence = [] + aux_sequence = {} + last_state = None + for idx, frame in enumerate(frames): + + aux_sequence[frame] = frame_loadTargets(setup, idx, sim_transform, dataset) + + # compatibility + vel_type_input_features = load_setup.training.velocity.decoder.get("type_input_features", \ + ["TARGET_RAW_UNPROJECTION"] if load_setup.training.velocity.decoder.input_type=='RAW' else ["TARGET_UNPROJECTION"]) + + velocity_grid = None + if velocity_decoder_model is not None: + velocity_grid = NeuralVelocityGrid(volume_decoder=velocity_decoder_model, boundary=vel_bounds, \ + scale_renderer=scale_renderer, warp_renderer=warp_renderer, device=resource_device, parent_state=None, \ + velocity_format=load_setup.training.velocity.decoder.velocity_format, \ + step_input_density=load_setup.training.velocity.decoder.step_input_density, \ + step_input_density_target=load_setup.training.velocity.decoder.get("step_input_density_target", []), \ + step_input_density_proxy=load_setup.training.velocity.decoder.get("step_input_density_proxy", []), \ + step_input_features=load_setup.training.velocity.decoder.step_input_features, \ + type_input_features=vel_type_input_features, \ + warp_input_indices=load_setup.training.velocity.decoder.warp_input_indices, \ + downscale_input_modes=load_setup.training.velocity.decoder.get("downscale_input_modes", ["RESAMPLE"])) + velocity_grid.use_raw_images = load_setup.training.velocity.decoder.input_type=='RAW' + velocity_grid.set_input_encoder(vel_input_encoder_model) + velocity_grid.set_downscale_encoder(vel_downscale_encoder_model) + else: + velocity_grid = VelocityGrid(GridShape.from_tensor(aux_sequence[frame].density).spatial_vector.as_shape, 0.0, boundary=vel_bounds, scale_renderer=scale_renderer, warp_renderer=warp_renderer, device=resource_device, var_name='vel_dummy_f{:06d}'.format(frame)) + + density_grid = None + density_proxy = None + if density_decoder_model is not None: + density_grid = NeuralDensityGrid(volume_decoder=density_decoder_model, scale_renderer=scale_renderer, parent_state=None, \ + base_input=load_setup.training.density.decoder.base_input, \ + step_input_density=load_setup.training.density.decoder.step_input_density, \ + step_input_density_target=load_setup.training.density.decoder.step_input_density_target, \ + step_input_features=load_setup.training.density.decoder.step_input_features, \ + type_input_features=load_setup.training.density.decoder.type_input_features, \ + device=resource_device, is_SDF=setup.data.SDF, base_SDF_mode=setup.training.density.decoder.base_SDF_mode) + density_grid.use_raw_images = load_setup.training.density.decoder.input_type=='RAW' + density_proxy = NeuralDensityGrid(volume_decoder=density_decoder_model, scale_renderer=scale_renderer, parent_state=None, \ + base_input=load_setup.training.density.decoder.base_input, \ + step_input_density=load_setup.training.density.decoder.step_input_density, \ + step_input_density_target=load_setup.training.density.decoder.step_input_density_target, \ + step_input_features=load_setup.training.density.decoder.step_input_features, \ + type_input_features=load_setup.training.density.decoder.type_input_features, \ + device=resource_device, is_SDF=setup.data.SDF, base_SDF_mode=setup.training.density.decoder.base_SDF_mode) + density_proxy.use_raw_images = load_setup.training.density.decoder.input_type=='RAW' + else: + dens_hull = None + density_grid = DensityGrid(GridShape.from_tensor(aux_sequence[frame].density).spatial_vector.as_shape, d=aux_sequence[frame].density, as_var=False, \ + scale_renderer=scale_renderer, hull=dens_hull, inflow=None, inflow_offset=None, inflow_mask=None, \ + device=resource_device, restrict_to_hull=setup.training.density.use_hull, is_SDF=setup.data.SDF) + log.debug("Initialized density with loaded data") + + state = NeuralState(density_grid, velocity_grid, target_encoder=target_encoder_model, encoder_output_types=load_setup.training.view_encoder.encoder, \ + target_lifting=load_setup.training.view_encoder.lifting, lifting_renderer=lifting_renderer, target_merging=load_setup.training.view_encoder.merge, \ + volume_encoder=volume_encoder_model, frame=0, transform=sim_transform.copy_no_data(), lifting_network=lifting_network_model, frame_merge_network=frame_merge_network_model) + + state.frame = frame + state_set_targets(state, aux_sequence) + #dataset.step() + state.data_path = os.path.join(setup.paths.data, 'frame_{:06d}'.format(state.frame)) + + state.density_proxy = density_proxy + + #set volume target data if needed/available + if ("density" in aux_sequence[frame]) and (aux_sequence[frame].density is not None): + state.density_target = frame_densTargetSetup(aux_sequence, frame) + if ("velocity" in aux_sequence[frame]) and (aux_sequence[frame].velocity is not None): + state.velocity_target = frame_velTargetSetup(aux_sequence, frame) + + state.base_target_cameras = setup_target_cameras(target_cameras, train_cam_resolution, None, setup.rendering.target_cameras.crop_frustum_pad) + #state_set_targets() + if False: + state_randomize(state, randomize_transform=True) + log.info("state.transform: %s", state.transform) + + state.prev = last_state + if last_state is not None: last_state.next = state + last_state = state + + if density_grid is not None: + density_grid.parent_state = state + if density_decoder_model is not None and load_setup.training.density.decoder.recursive_MS: #setup.training.density.decoder.active + #setup recusive MS + num_levels = get_max_recursive_MS_grow_levels(setup.training.density.decoder) + #log.debug("%s", setup.training.velocity.decoder.recursive_MS_levels) + # TODO: check trained levels and shared decoder + if not load_setup.training.density.decoder.recursive_MS_shared_model: + if num_levels!=len(density_decoder_model): + log.error("recursive multi-scale density is fixed to %d recursions, %d are requested.", len(density_decoder_model), num_levels) + raise SystemExit(1) + scale_factor = setup.training.density.decoder.recursive_MS_scale_factor + if load_setup.training.density.decoder.recursive_MS_scale_factor!=scale_factor: + log.warning("network was trained with a resolution scale-factor of %f, evaluation uses %f", load_setup.training.density.decoder.recursive_MS_scale_factor, scale_factor) + density_grid.set_recursive_MS(num_levels, scale_factor, shared_decoder=load_setup.training.density.decoder.recursive_MS_shared_model, train_mode=load_setup.training.density.decoder.recursive_MS_train_mode, as_residual=setup.training.density.decoder.recursive_MS_residual, direct_input=setup.training.density.decoder.get("recursive_MS_direct_input", False)) + if density_proxy is not None: + density_proxy.parent_state = state + if density_decoder_model is not None and load_setup.training.density.decoder.recursive_MS: + num_levels = get_max_recursive_MS_grow_levels(setup.training.density.decoder) + scale_factor = setup.training.density.decoder.recursive_MS_scale_factor + density_proxy.set_recursive_MS(num_levels, scale_factor, shared_decoder=load_setup.training.density.decoder.recursive_MS_shared_model, train_mode=load_setup.training.density.decoder.recursive_MS_train_mode, as_residual=setup.training.density.decoder.recursive_MS_residual, direct_input=setup.training.density.decoder.get("recursive_MS_direct_input", False)) + if velocity_grid is not None: + velocity_grid.parent_state = state + if setup.training.velocity.decoder.active and load_setup.training.velocity.decoder.recursive_MS: + #setup recusive MS + num_levels = get_max_recursive_MS_grow_levels(setup.training.velocity.decoder) + #log.debug("%s", setup.training.velocity.decoder.recursive_MS_levels) + # TODO: check trained levels and shared decoder + if not load_setup.training.velocity.decoder.recursive_MS_shared_model: + if num_levels!=len(velocity_decoder_model): + log.error("recursive multi-scale velocity is fixed to %d recursions, %d are requested.", len(velocity_decoder_model), num_levels) + raise SystemExit(1) + scale_factor = setup.training.velocity.decoder.recursive_MS_scale_factor #load_setup.training.velocity.decoder.model.level_scale_factor #allow changing this for evaluation + if load_setup.training.velocity.decoder.recursive_MS_scale_factor!=scale_factor: + log.warning("network was trained with a resolution scale-factor of %f, evaluation uses %f", load_setup.training.velocity.decoder.recursive_MS_scale_factor, scale_factor) + velocity_grid.set_recursive_MS(num_levels, scale_factor, shared_decoder=load_setup.training.velocity.decoder.recursive_MS_shared_model, train_mode=load_setup.training.velocity.decoder.recursive_MS_train_mode, direct_input=setup.training.velocity.decoder.get("recursive_MS_direct_input", False), max_level_input=setup.training.velocity.decoder.get("recursive_MS_use_max_level_input", False)) + #log.info("Set recursive-MS velocity for frame %d with %d levels.", frame, num_levels) + + + sequence.append(state) + return Sequence(sequence), aux_sequence + + + sequence, eval_sequence = build_sequence(frames, target_dataset) + log.debug('Load run targets') + + + scalar_results = munch.Munch() + final_transform = sim_transform.copy_no_data() + final_transform.grid_size = sequence[0].transform.grid_size if setup.training.density.decoder.active else sequence[0].density.shape + scalar_results.sim_transform = final_transform + with open(os.path.join(setup.paths.data, "scalar_results.json"), "w") as f: + try: + json.dump(scalar_results, f, default=tf_to_dict, sort_keys=True, indent=2) + except: + log.exception("Failed to write scalar_results:") + + if False: + log.warning("LR vel eval: [8,8,8], level 1") + for state in sequence: + state.velocity.set_centered_shape([8,8,8]) + state.velocity.set_recursive_MS_level(1) + + z = None #tf.zeros([1] + vel_shape + [1]) + + # vel_scale = world_scale(vel_shape, width=1.) + + if setup.validation.stats or setup.validation.warp_test or args.render: + + def print_stats_dict(stats, name, print_fn): + s = '{}:\n'.format(name) + for name in sorted(stats.keys()): + value = stats[name] + if isinstance(value, tf.Tensor): + value = value.numpy().tolist() + if not isinstance(value, float): + s += '{:<16}: {}\n'.format(name, value) + else: + s += '{:<16}: {: 13.06e}\n'.format(name, value) + print_fn(s) + + def render_sequence_cmp(*sequences, cameras, path, name_pre='seq', image_format='PNG', render_velocity=True, background="COLOR", crop_cameras=True): + #sequences: iterables of states to render or lists with images for every camera. + assert len(sequences)>1, "need at least 2 sequences to compare" + length = len(sequences[0]) + for sequence in sequences: + assert len(sequence)==length, "All sequences must have equal length" + log.debug("Render comparison of %d sequences:", len(sequences)) + # render image cmp + AABB_corners_WS = [] + for states in zip(*sequences): + for state in states: + if isinstance(state, State): + dens_hull = state.density.hull #state.hull if hasattr(state, "hull") else + if dens_hull is None: + continue + dens_transform = state.get_density_transform() + AABB_corners_WS += dens_transform.transform_AABB(*hull_AABB_OS(tf.squeeze(dens_hull, (0,-1))), True) + if AABB_corners_WS and crop_cameras: + seq_cams = [cam.copy_clipped_to_world_coords(AABB_corners_WS)[0] for cam in cameras] + else: + seq_cams = cameras + split_cams = True + i=0 + for states in zip(*sequences): + log.debug("Render sequence cmp frame %d", i) + # density: [orig, dens_warp, veldens_warp] + if args.console: progress_bar(i*2,len(sequences[0])*2, "Step {:03d}/{:03d}: {:30}".format(i+1,len(sequence), "Sequence cmp Density"), length=30) + bkg_render = None + if background=='COLOR': + bkg_render = [tf.constant(setup.rendering.background.color, dtype=tf.float32)]*len(seq_cams) + if isinstance(background, list): + bkg_render = background[i] + if isinstance(background, (np.ndarray, tf.Tensor)): + bkg_render = background + #sim_transform.set_data(state.density) + dens_imgs = [] + for state in states: + if isinstance(state, State): + dens_imgs.append(tf.concat(renderer.render_density_SDF_switch(state.get_density_transform(), lights, seq_cams, background=bkg_render, split_cameras=split_cams), axis=0)) + elif isinstance(state, (list, tuple)): + state = tf.concat(state, axis=0) + if isinstance(state, (np.ndarray, tf.Tensor)): + state_shape = shape_list(state) + if len(state_shape)!=4 or state_shape[0]!=len(seq_cams): + raise ValueError + if state_shape[-1]==1: + state = tf.tile(state, (1,1,1,3)) + dens_imgs.append(tf.identity(state)) + + renderer.write_images([tf.concat(dens_imgs, axis=-2)], [name_pre + '_cmp_dens_cam{}_{:04d}'], base_path=path, use_batch_id=True, frame_id=i, format=image_format) + + # velocity: [orig, veldens_warp] + if render_velocity: + vel_imgs = [] + if args.console: progress_bar(i*2+1,len(sequence)*2, "Step {:03d}/{:03d}: {:30}".format(i+1,len(sequence), "Sequence cmp Velocity"), length=30) + for state in states: + if isinstance(state, State): + vel_transform = state.get_velocity_transform() + vel_scale = vel_transform.cell_size_world().value + log.debug("Render velocity frame %d with cell size %s", i, vel_scale) + vel_centered = state.velocity.centered() * get_vel_scale_for_render(setup, vel_transform)#vel_scale/float(setup.data.step)*setup.rendering.velocity_scale + vel_imgs.append(tf.concat(vel_renderer.render_density(vel_transform, [tf.abs(vel_centered)], cameras, split_cameras=split_cams), axis=0)) + vel_renderer.write_images([tf.concat(vel_imgs, axis=-2)], [name_pre + '_cmp_velA_cam{}_{:04d}'], base_path=path, use_batch_id=True, frame_id=i, format=image_format) + + i+=1 + + def get_frame_stats(state, mask=None, cmp_vol_targets=False): + stats = {} + try: + with profiler.sample("stats"): + dens_stats, vel_stats, tar_stats = state.stats(render_ctx=main_render_ctx, dt=1.0, order=setup.training.velocity.warp_order, clamp=setup.training.density.warp_clamp)#vel_scale) + vel_stats['scale'] = world_scale(state.velocity.centered_shape, width=1.) + if mask is not None: + dens_hull_stats, vel_hull_stats, _ = state.stats(mask=mask, dt=1.0, order=setup.training.velocity.warp_order, clamp=setup.training.density.warp_clamp) + vel_hull_stats['scale'] = world_scale(state.velocity.centered_shape, width=1.) + + + if cmp_vol_targets: + vTar_dens_stats, vTar_vel_stats = state.stats_target(dt=1.0, order=setup.training.velocity.warp_order, clamp=setup.training.density.warp_clamp) + if mask is not None: + if mask.dtype!=tf.bool: + mask = tf.not_equal(mask, 0) + vTar_dens_hull_stats, vTar_vel_hull_stats = state.stats_target(mask=mask, dt=1.0, order=setup.training.velocity.warp_order, clamp=setup.training.density.warp_clamp) + + + if state.has_density_target: + stats["vTar_density"]=vTar_dens_stats + dens_SE = (state.density_target.d - state.density.d)**2 + dens_stats['_vTar_SE'] = tf_tensor_stats(dens_SE, as_dict=True) + if mask is not None: + stats["vTar_density_hull"]=vTar_dens_hull_stats + dens_hull_stats['_vTar_SE'] = tf_tensor_stats(tf.boolean_mask(dens_SE, mask), as_dict=True) + if state.has_velocity_target: + stats["vTar_velocity"]=vTar_vel_stats + vel_diffMag = (state.velocity_target - state.velocity).magnitude() + vel_CangleRad_mask = tf.greater(state.velocity_target.magnitude() * state.velocity.magnitude(), 1e-8) + vel_CangleRad = tf_angle_between(state.velocity_target.centered(), state.velocity.centered(), axis=-1, keepdims=True) + vel_stats['_vTar_vdiff_mag'] = tf_tensor_stats(vel_diffMag, as_dict=True) + vel_stats['_vTar_angleCM_rad'] = tf_tensor_stats(tf.boolean_mask(vel_CangleRad, vel_CangleRad_mask), as_dict=True) + if mask is not None: + stats["vTar_velocity_hull"]=vTar_vel_hull_stats + vel_hull_stats['_vTar_vdiff_mag'] = tf_tensor_stats(tf.boolean_mask(vel_diffMag, mask), as_dict=True) + vel_hull_stats['_vTar_angleCM_rad'] = tf_tensor_stats(tf.boolean_mask(vel_CangleRad, tf.logical_and(mask, vel_CangleRad_mask)), as_dict=True) + + stats["density"]=dens_stats + stats["velocity"]=vel_stats + stats["target"]=tar_stats + if mask is not None: + stats["density_hull"]=dens_hull_stats + stats["velocity_hull"]=vel_hull_stats + except: + log.exception("Exception during reconstruction stats of frame %d", state.frame) + return stats + + if setup.data.randomize>0: #args.fit and + log.info('Setting validation data for evaluation.') + for state in sequence: + state.clear_cache() + state_set_targets(state, val_sequence if args.fit else eval_sequence) + state_randomize(state, disable_transform_reset=True) #reset randomization + state.input_view_mask = setup.validation.input_view_mask + + if velocity_decoder_model is not None: + if density_decoder_model is not None: + log.info("Set sequence densities for neural globt.") + sequence.set_density_for_neural_globt(order=setup.training.velocity.warp_order, clamp=('NONE' if setup.training.density.warp_clamp=='NEGATIVE' else setup.training.density.warp_clamp), device=resource_device) + sequence.clear_cache() + + if setup.validation.stats: + log.info("Data Statistics") + stats_file = os.path.join(setup.paths.log, "stats.json") + stats_dict = {} + frame_keys = [] + for state in sequence: + frame_key = "{:04d}".format(state.frame) + frame_keys.append(frame_key) + stats_mask = state.density.hull #tf.greater(state.density.hull, 0.5) + stats_dict[frame_key] = get_frame_stats(state=state, mask=state.density.hull, cmp_vol_targets=setup.validation.cmp_vol_targets) + + try: + json_dump(stats_file, stats_dict, compressed=True, default=tf_to_dict, sort_keys=True) + except: + log.exception("Failed to write stats:") + del stats_dict + + + if args.render: + try: + log.info('Render final output.') + + + render_sequence(sequence, z, cycle=setup.validation.render_cycle, cycle_steps=setup.validation.render_cycle_steps, \ + sF_cam=setup.validation.render_target, \ + render_density=setup.validation.render_density, render_shadow=setup.validation.render_shadow, \ + render_velocity=setup.validation.render_velocity, render_MS=setup.validation.render_MS, \ + slices = ['X','Y','Z']) + except KeyboardInterrupt: + log.warning("Interrupted final output rendering.") + except: + log.exception("Error during final output rendering:") + + + if args.save_volume: + try: + log.info('Saving volumes.') + sequence.save() + except KeyboardInterrupt: + log.warning("Interrupted saving volumes.") + except: + log.exception("Error during saving volumes:") + + #render_sequence(sequence, vel_scale, z, cycle=True, cycle_steps=12, sF_cam=True) + + used_mem = tf.contrib.memory_stats.MaxBytesInUse().numpy().tolist() + max_mem = tf.contrib.memory_stats.BytesLimit().numpy().tolist() + log.info('GPU memory usage: max: %d MiB (%.02f%%), limit: %d MiB', \ + used_mem/(1024*1024), (used_mem/max_mem)*100.0, max_mem/(1024*1024)) + + with open(os.path.join(setup.paths.log, 'profiling.txt'), 'w') as f: + profiler.stats(f) + #profiler.stats() + log.info("DONE") + logging.shutdown() + sys.exit(0) diff --git a/scalaFlow_cameras.json b/scalaFlow_cameras.json new file mode 100644 index 0000000..10a9d6b --- /dev/null +++ b/scalaFlow_cameras.json @@ -0,0 +1,173 @@ +{ + "0": { + "forward": [ + 0.02991165315231203, + 0.2862955301699637, + 0.9576743509201837 + ], + "fov_horizontal": 23.78101295049065, + "fov_vertical": 40.48912068873593, + "position": [ + 0.2844361227091017, + 0.011681095083065586, + -0.985782994544316 + ], + "position_error": 5.225227536501316e-05, + "right": [ + 0.999482898511128, + -0.004169059649956844, + -0.03188345221943645 + ], + "rotation": [ + 16.636304218500776, + 1.788973998253707, + 0.0 + ], + "up": [ + -0.0012139369455675866, + 0.9581538439081456, + -0.2862511794930057 + ] + }, + "1": { + "forward": [ + 0.5615468372321614, + 0.2879380447290926, + 0.7757298705039941 + ], + "fov_horizontal": 22.582333954355, + "fov_vertical": 40.46782568763134, + "position": [ + -0.39192498154812905, + 0.010452679354390565, + -0.8086869307225988 + ], + "position_error": 0.000617079470636767, + "right": [ + 0.8155876490580826, + -0.027892342715864132, + -0.5779609017240966 + ], + "rotation": [ + 16.734549964915107, + 35.900584467250546, + 0.0 + ], + "up": [ + -0.12405813576773944, + 0.9554980240982053, + -0.2676436154558124 + ] + }, + "2": { + "forward": [ + 0.839886163674708, + 0.28936547025845033, + 0.4591937027985984 + ], + "fov_horizontal": null, + "fov_vertical": 40.4354766330021, + "position": [ + -0.7697112134249342, + 0.013170702826451303, + -0.32495259467201193 + ], + "position_error": 0.003692911448042811, + "right": [ + 0.4877814604833295, + -0.02067908593551731, + -0.8727208157329706 + ], + "rotation": [ + 16.819971471487463, + 61.333105216300034, + 0.0 + ], + "up": [ + -0.24484249213150328, + 0.9571192369278678, + -0.15483836846676868 + ] + }, + "3": { + "forward": [ + -0.747669825922995, + 0.27302277772441563, + 0.6053498114709565 + ], + "fov_horizontal": null, + "fov_vertical": 41.234227376207365, + "position": [ + 1.2956339472855067, + 0.024366562358338006, + -0.4975469230725041 + ], + "position_error": 0.00017112898022086318, + "right": [ + 0.6348187184633104, + 0.01937940769887405, + 0.7724180430607774 + ], + "rotation": [ + 15.844219215313926, + -51.00468761390708, + 0.0 + ], + "up": [ + 0.206020666527721, + 0.9616391003650987, + -0.18112350927606619 + ] + }, + "4": { + "forward": [ + -0.44314704373377406, + 0.28570227819571825, + 0.8497028338565153 + ], + "fov_horizontal": 23.674657446663893, + "fov_vertical": 40.67651218062054, + "position": [ + 0.8974969740854534, + 0.026809888348629074, + -0.8316138645339368 + ], + "position_error": 4.6716698103299165e-05, + "right": [ + 0.8837605181615485, + -0.023959524804196622, + 0.46732567627938887 + ], + "rotation": [ + 16.600831694196863, + -27.543473127902832, + 0.0 + ], + "up": [ + 0.1498681935496668, + 0.95826194909627, + -0.2434616221838202 + ] + }, + "focus": [ + 0.3382070094283088, + 0.38795384153014023, + 0.2609209839653898 + ], + "focus_error": 0.0012484445282397506, + "fov_horizontal_average": 23.34600145050318, + "fov_vertical_average": 40.660632513239456, + "marker_width": 0.4909, + "scale_y": 1.77, + "volume_offset": [ + 0.08181666666666666, + -0.04462727272727273, + -0.004909 + ], + "volume_size": [ + 0.4909, + 0.868893, + 0.4909 + ], + "volume_width": 0.4909 +} \ No newline at end of file