├── .gitignore ├── .gitmodules ├── CMakeLists.txt ├── LICENSE ├── README.md ├── results ├── blue_noise_128.bmp ├── blue_noise_128_freq.bmp ├── blue_noise_32.bmp ├── blue_noise_32_freq.bmp ├── gt.png ├── spp1.png ├── spp1_dither_from_256.png ├── spp256.png ├── white_noise_128.bmp ├── white_noise_128_freq.bmp ├── white_noise_32.bmp └── white_noise_32_freq.bmp ├── scripts └── vis_image_freq.py └── src ├── CImg.h ├── generate_blue_noise.cpp ├── generate_blue_noise.h ├── image.h ├── main.cpp └── py_wrapper.cpp /.gitignore: -------------------------------------------------------------------------------- 1 | build/* -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "pybind11"] 2 | path = pybind11 3 | url = https://github.com/pybind/pybind11.git 4 | [submodule "pbar"] 5 | path = pbar 6 | url = https://github.com/Jvanrhijn/pbar.git 7 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required (VERSION 2.8.3) 2 | 3 | project(BlueNoise) 4 | if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) 5 | message(STATUS "Setting build type to 'Release' as none was specified.") 6 | set(CMAKE_BUILD_TYPE Release CACHE STRING "Choose the type of build." FORCE) 7 | set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release" 8 | "MinSizeRel" "RelWithDebInfo") 9 | endif() 10 | 11 | IF(CMAKE_BUILD_TYPE MATCHES RELEASE) 12 | ADD_DEFINITIONS (-DNDEBUG) 13 | ENDIF() 14 | 15 | 16 | if(NOT IS_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/pybind11") 17 | message(FATAL_ERROR "The dependency repositories (pybind11, pbar, etc.) are missing! " 18 | "You probably did not clone the project with --recursive. It is possible to recover " 19 | "by calling \"git submodule update --init --recursive" 20 | "Calling git -c submodule."thirdparty/xxx".update=none submodule update --init --recursive to ignore specific library.\"") 21 | endif() 22 | 23 | 24 | 25 | # Enable C++11 mode on GCC / Clang 26 | if (CMAKE_CXX_COMPILER_ID MATCHES "GNU") 27 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=gnu++11") 28 | elseif (CMAKE_CXX_COMPILER_ID MATCHES "Clang") 29 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11") 30 | endif() 31 | 32 | if (MSVC) 33 | add_definitions (/D "_CRT_SECURE_NO_WARNINGS") 34 | add_definitions (/D "__TBB_NO_IMPLICIT_LINKAGE") 35 | 36 | # Parallel build on MSVC (all targets) 37 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /MP") 38 | # Static build 39 | set(CompilerFlags 40 | CMAKE_CXX_FLAGS CMAKE_CXX_FLAGS_DEBUG CMAKE_CXX_FLAGS_RELEASE 41 | CMAKE_CXX_FLAGS_MINSIZEREL CMAKE_CXX_FLAGS_RELWITHDEBINFO 42 | CMAKE_C_FLAGS CMAKE_C_FLAGS_DEBUG CMAKE_C_FLAGS_RELEASE 43 | CMAKE_C_FLAGS_MINSIZEREL CMAKE_C_FLAGS_RELWITHDEBINFO) 44 | foreach(CompilerFlag ${CompilerFlags}) 45 | string(REPLACE "/MD" "/MT" ${CompilerFlag} "${${CompilerFlag}}") 46 | endforeach() 47 | endif() 48 | 49 | find_package(OpenMP REQUIRED) # Find the package 50 | if(OPENMP_FOUND) 51 | set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${OpenMP_C_FLAGS}") 52 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${OpenMP_CXX_FLAGS}") 53 | set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} ${OpenMP_EXE_LINKER_FLAGS}") 54 | endif() 55 | 56 | 57 | INCLUDE_DIRECTORIES("${CMAKE_CURRENT_SOURCE_DIR}/pbar") 58 | 59 | add_executable(BlueNoiseGenerator 60 | src/main.cpp src/generate_blue_noise.cpp src/generate_blue_noise.h) 61 | 62 | add_subdirectory(pybind11) 63 | pybind11_add_module(PyBlueNoiseGenerator src/py_wrapper.cpp src/generate_blue_noise.h src/generate_blue_noise.cpp) 64 | 65 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 gao-duan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Blue Noise 2 | 3 | by [Duan Gao](https://gao-duan.github.io/) 4 | 5 | Implement the algorithm introduced in [1]. 6 | 7 | The blue noise image can be used in Path Tracing to distribute the Monte Carlo noise from white noise to blue noise (I have tested this idea in my own physically based renderer [Elegans](https://github.com/gao-duan/Elegans)) , more details are described in [2]. 8 | 9 | **Update[2020-01-06]** 10 | 11 | - Add Python binding support (via [pybind11](https://github.com/pybind/pybind11/tree/98f1bbb8004f654ba9e26717bdf5912fb899b05a)) 12 | - Add support for generating multiple dimensional blue noise texture. (e.g 2D/3D blue noise texture) 13 | 14 | ## Dependencies 15 | 16 | - [CImg](https://github.com/dtschump/CImg) 17 | - OpenMP support 18 | - C++ 11 19 | - [pybind11](https://github.com/pybind/pybind11) 20 | - [pbar](https://github.com/Jvanrhijn/pbar) 21 | 22 | ## Results 23 | 24 | 1D blue-noise: 25 | 26 | | Resolution | White noise | White noise FFT [3] | Blue noise | Blue noise FFT | 27 | | ---------- | --------------------------------- | -------------------------------------- | ---------- | -------------- | 28 | | 32x32 | ![](./results/white_noise_32.bmp) | ![](./results/white_noise_32_freq.bmp) | ![](./results/blue_noise_32.bmp) | ![](./results/blue_noise_32_freq.bmp) | 29 | | 128x128 | ![](./results/white_noise_128.bmp) | ![](./results/white_noise_128_freq.bmp) | ![](./results/blue_noise_128.bmp) | ![](./results/blue_noise_128_freq.bmp) | 30 | 31 | 32 | 33 | Here we show the path tracing results comparison of Cornell Box (only 1spp) : 34 | 35 | > Here I just implement the sorting scheme introduced in the slides [4]. 36 | > 37 | > - sorting all the radiances of same pixel; 38 | > - pick the radiance according to the blue noise value in the same pixel position. 39 | 40 | 41 | 42 | | | path tracing(spp=1) | dithering(spp=1 from 256) | path tracing(spp=256) | reference | 43 | | ----- | :---------------------: | :-------------------------------------: | :-----------------------: | :-------------------: | 44 | | Image | ![](./results/spp1.png) | ![](./results/spp1_dither_from_256.png) | ![](./results/spp256.png) | ![](./results/gt.png) | 45 | | RMSE | 0.04736 | 0.04778 | 0.003352 | - | 46 | 47 | 48 | 49 | 50 | [1] Georgiev I, Fajardo M. Blue-noise dithered sampling[C]//ACM SIGGRAPH 2016 Talks. ACM, 2016: 35. 51 | 52 | https://www.arnoldrenderer.com/research/dither_abstract.pdf 53 | 54 | [2] Heitz E, Belcour L. Distributing Monte Carlo Errors as a Blue Noise in Screen Space by Permuting Pixel Seeds Between Frames[C]//Computer Graphics Forum. 2019, 38(4): 149-158. 55 | 56 | https://hal.archives-ouvertes.fr/hal-02158423/document 57 | 58 | [3] Use ./results/vis_image_freq.py to generate the frequency visualization image. Please see [OpenCV](https://docs.opencv.org/3.0-beta/doc/py_tutorials/py_imgproc/py_transforms/py_fourier_transform/py_fourier_transform.html) for more details. 59 | 60 | [4] https://hal.archives-ouvertes.fr/hal-02158423/file/blueNoiseTemporal2019_slides.pdf 61 | -------------------------------------------------------------------------------- /results/blue_noise_128.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gao-duan/BlueNoise/8541c129933bdd8ccb17602e3f50a29149d6a292/results/blue_noise_128.bmp -------------------------------------------------------------------------------- /results/blue_noise_128_freq.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gao-duan/BlueNoise/8541c129933bdd8ccb17602e3f50a29149d6a292/results/blue_noise_128_freq.bmp -------------------------------------------------------------------------------- /results/blue_noise_32.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gao-duan/BlueNoise/8541c129933bdd8ccb17602e3f50a29149d6a292/results/blue_noise_32.bmp -------------------------------------------------------------------------------- /results/blue_noise_32_freq.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gao-duan/BlueNoise/8541c129933bdd8ccb17602e3f50a29149d6a292/results/blue_noise_32_freq.bmp -------------------------------------------------------------------------------- /results/gt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gao-duan/BlueNoise/8541c129933bdd8ccb17602e3f50a29149d6a292/results/gt.png -------------------------------------------------------------------------------- /results/spp1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gao-duan/BlueNoise/8541c129933bdd8ccb17602e3f50a29149d6a292/results/spp1.png -------------------------------------------------------------------------------- /results/spp1_dither_from_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gao-duan/BlueNoise/8541c129933bdd8ccb17602e3f50a29149d6a292/results/spp1_dither_from_256.png -------------------------------------------------------------------------------- /results/spp256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gao-duan/BlueNoise/8541c129933bdd8ccb17602e3f50a29149d6a292/results/spp256.png -------------------------------------------------------------------------------- /results/white_noise_128.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gao-duan/BlueNoise/8541c129933bdd8ccb17602e3f50a29149d6a292/results/white_noise_128.bmp -------------------------------------------------------------------------------- /results/white_noise_128_freq.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gao-duan/BlueNoise/8541c129933bdd8ccb17602e3f50a29149d6a292/results/white_noise_128_freq.bmp -------------------------------------------------------------------------------- /results/white_noise_32.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gao-duan/BlueNoise/8541c129933bdd8ccb17602e3f50a29149d6a292/results/white_noise_32.bmp -------------------------------------------------------------------------------- /results/white_noise_32_freq.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gao-duan/BlueNoise/8541c129933bdd8ccb17602e3f50a29149d6a292/results/white_noise_32_freq.bmp -------------------------------------------------------------------------------- /scripts/vis_image_freq.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import cv2 3 | import argparse 4 | 5 | parser = argparse.ArgumentParser() 6 | parser.add_argument('-i', '--input_file', type=str, required=True) 7 | parser.add_argument('-o', '--output_file', type=str, required=True) 8 | 9 | args,unknown = parser.parse_known_args() 10 | 11 | if __name__ == '__main__': 12 | input_file = args.input_file 13 | output_file = args.output_file 14 | 15 | raw_img = cv2.imread(input_file)[...,::-1] 16 | 17 | channels = np.split(raw_img, raw_img.shape[2], axis = -1) 18 | 19 | res = [] 20 | for img in channels: 21 | img_float32 = np.float32(img) 22 | dft = cv2.dft(img_float32, flags = cv2.DFT_COMPLEX_OUTPUT) 23 | dft_shift = np.fft.fftshift(dft) 24 | magnitude_spectrum = np.log(cv2.magnitude(dft_shift[:,:,0],dft_shift[:,:,1]) + 1e-20) 25 | magnitude_spectrum = (magnitude_spectrum - np.min(magnitude_spectrum)) / (np.max(magnitude_spectrum) - np.min(magnitude_spectrum) + 1e-10) 26 | 27 | res.append(magnitude_spectrum[..., np.newaxis]) 28 | 29 | res = np.concatenate(res, axis = -1) 30 | print(np.mean(res)) 31 | cv2.imwrite(output_file, res[..., ::-1] ** (1.2) * 255) -------------------------------------------------------------------------------- /src/generate_blue_noise.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "image.h" 5 | #include "pbar.h" 6 | #include "generate_blue_noise.h" 7 | 8 | 9 | void SaveImage(const std::vector >& img, int x_resolution, int y_resolution, const std::string& path, float gamma) { 10 | ImageRGB img_white(x_resolution, y_resolution); 11 | for (int x = 0; x < x_resolution; ++x) { 12 | for (int y = 0; y < y_resolution; ++y) { 13 | std::vector v = img[y * x_resolution + x]; 14 | img_white.Set(x, y, v); 15 | } 16 | } 17 | img_white.save(path, gamma); 18 | } 19 | 20 | // random float number in range [0,1] 21 | float NextFloat() { 22 | return static_cast (rand()) / static_cast (RAND_MAX); 23 | } 24 | 25 | // distance between two 2D points 26 | float DistanceSquared(float x1, float y1, float x2, float y2) { 27 | return (x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1); 28 | } 29 | 30 | float DistanceSamples(const std::vector& p, const std::vector& q) { 31 | const int d = p.size(); 32 | float res = 0.0f; 33 | for (size_t i = 0; i < p.size(); ++i) { 34 | res += std::pow(std::abs(p[i] - q[i]), (d / 2.0)); 35 | } 36 | return res; 37 | } 38 | 39 | 40 | void BlueNoiseGenerator::_init() 41 | { 42 | for (size_t i = 0; i < x_resolution * y_resolution; ++i) { 43 | for (size_t d = 0; d < depth; ++d) { 44 | float _t = NextFloat(); 45 | white_noise[i].push_back(_t); 46 | blue_noise[i].push_back(_t); 47 | } 48 | } 49 | } 50 | 51 | float BlueNoiseGenerator::E() 52 | { 53 | float loss = 0; 54 | const float sigma_i = 2.1f; 55 | const float sigma_s = 1.0f; 56 | 57 | #pragma omp parallel for num_threads(threads) reduction(+: loss) // use openmp 58 | for (int px = 0; px < x_resolution; ++px) { 59 | for (int py = 0; py < y_resolution; ++py) { 60 | int i = px * x_resolution + py; 61 | std::vector ps = blue_noise[i]; 62 | 63 | float pix = i / x_resolution, piy = i % x_resolution; 64 | 65 | if (kernel_size == -1) { 66 | // use all pixel pairs to compute the loss 67 | for (int qx = 0; qx < x_resolution; ++qx) { 68 | for (int qy = 0; qy < y_resolution; ++qy) { 69 | float qix = qx, qiy = qy; 70 | 71 | float pixel_distance = DistanceSquared(pix, piy, qix, qiy); 72 | 73 | int qqx = (qx + x_resolution) % x_resolution; 74 | int qqy = (qy + y_resolution) % y_resolution; 75 | 76 | int j = qqx * x_resolution + qqy; 77 | if (j == i) continue; 78 | std::vector qs = blue_noise[j]; 79 | 80 | float sample_distance = DistanceSamples(ps, qs); 81 | loss += std::exp( 82 | -(pixel_distance) / (sigma_i * sigma_i) 83 | - (sample_distance) / (sigma_s * sigma_s)); 84 | } 85 | } 86 | } 87 | else { 88 | // use all neighborhood around the pixel (px,py) to approximate the loss 89 | for (int qx = pix - kernel_size; qx <= pix + kernel_size; ++qx) { 90 | for (int qy = piy - kernel_size; qy <= piy + kernel_size; ++qy) { 91 | float qix = qx, qiy = qy; 92 | 93 | float pixel_distance = DistanceSquared(pix, piy, qix, qiy); 94 | 95 | int qqx = (qx + x_resolution) % x_resolution; 96 | int qqy = (qy + y_resolution) % y_resolution; 97 | 98 | int j = qqx * x_resolution + qqy; 99 | if (j == i) continue; 100 | std::vector qs = blue_noise[j]; 101 | 102 | float sample_distance = DistanceSamples(ps, qs); 103 | loss += std::exp( 104 | -(pixel_distance) / (sigma_i * sigma_i) 105 | - (sample_distance) / (sigma_s * sigma_s)); 106 | } 107 | } 108 | } 109 | } 110 | } 111 | return loss; 112 | } 113 | 114 | void BlueNoiseGenerator::optimize(int max_iter, bool verbose) 115 | { 116 | if (verbose) { 117 | SaveImage(white_noise, x_resolution, y_resolution, "white_noise.png", 1.0f); 118 | } 119 | 120 | // 2. random swap two pixels to minimize the loss 121 | float loss = E(); 122 | int size = x_resolution * y_resolution; 123 | 124 | // pbar and verbose stuff 125 | int progress_iters = int(float(max_iter) / 10.0f); 126 | std::vector progress_v(max_iter); 127 | std::iota(progress_v.begin(), progress_v.end(), 0); 128 | int pbar_width = 50; 129 | if (!verbose) { 130 | pbar_width = 0; 131 | } 132 | pbar::ProgressBar::iterator> pbar(progress_v.begin(), progress_v.end(), pbar_width); 133 | 134 | 135 | for (auto _i = pbar.begin(); _i != pbar.end(); _i++) { 136 | int iter = *_i; 137 | int i = NextFloat() * (size - 1); 138 | int j = NextFloat() * (size - 1); 139 | if (i == j) continue; 140 | 141 | std::swap(blue_noise[i], blue_noise[j]); 142 | 143 | float new_loss = E(); 144 | 145 | // swap back. 146 | if (new_loss > loss) { 147 | std::swap(blue_noise[i], blue_noise[j]); 148 | } 149 | else { 150 | loss = new_loss; 151 | } 152 | 153 | if (verbose && iter % progress_iters == 0) { 154 | printf("loss: %0.4f, iter:%d\n", loss, iter); 155 | } 156 | 157 | if (verbose) { 158 | SaveImage(blue_noise, x_resolution, y_resolution, "blue_noise.png", 1.0f); 159 | } 160 | } 161 | } 162 | 163 | 164 | -------------------------------------------------------------------------------- /src/generate_blue_noise.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #ifndef __BLUE_NOISE_H__ 3 | #define __BLUE_NOISE_H__ 4 | 5 | #include 6 | 7 | struct BlueNoiseGenerator { 8 | std::vector> white_noise; 9 | std::vector> blue_noise; 10 | int x_resolution=0, y_resolution=0, depth=1; 11 | int kernel_size = -1; 12 | int threads = 4; 13 | 14 | BlueNoiseGenerator() {} 15 | 16 | BlueNoiseGenerator(int x_resolution, int y_resolution, int depth = 1, int kernel_size = -1) 17 | :x_resolution(x_resolution), y_resolution(y_resolution), depth(depth), kernel_size(kernel_size) { 18 | blue_noise.resize(x_resolution * y_resolution); 19 | white_noise.resize(x_resolution * y_resolution); 20 | _init(); 21 | } 22 | 23 | 24 | void optimize(int max_iter) { 25 | return optimize(max_iter, false); 26 | } 27 | 28 | void optimize(int max_iter, bool verbose); 29 | 30 | float E(); 31 | private: 32 | void _init(); 33 | }; 34 | 35 | #endif -------------------------------------------------------------------------------- /src/image.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "CImg.h" 3 | #include 4 | 5 | typedef float Float; 6 | 7 | 8 | struct Color { 9 | Float r, g, b; 10 | Color(Float _r = 0, Float _g = 0, Float _b = 0) :r(_r), g(_g), b(_b) {} 11 | 12 | Color operator*(Float f) { 13 | return Color(r * f, g * f, b * f); 14 | } 15 | Color operator-(const Color& c) { 16 | return Color(r - c.r, g - c.g, b - c.b); 17 | } 18 | Color operator+(const Color& c) { 19 | return Color(r + c.r, g + c.g, b + c.b); 20 | } 21 | Float operator[](int idx) const { 22 | if (idx == 0) return r; 23 | else if (idx == 1) return g; 24 | else return b; 25 | } 26 | Float& operator[](int idx) { 27 | if (idx == 0) return r; 28 | else if (idx == 1) return g; 29 | else return b; 30 | } 31 | }; 32 | 33 | Float Clamp(Float x, Float a, Float b) { 34 | Float res = x; 35 | if (res < a) res = a; 36 | if (res > b) res = b; 37 | return res; 38 | } 39 | Color Clamp(const Color& c, Float a, Float b) { 40 | Color res; 41 | res.r = Clamp(c.r, a, b); 42 | res.g = Clamp(c.g, a, b); 43 | res.b = Clamp(c.b, a, b); 44 | return res; 45 | } 46 | 47 | class ImageRGB { 48 | public: 49 | ImageRGB() :width(-1), height(-1) {} 50 | ImageRGB(int x, int y) 51 | :width(x), height(y) { 52 | pixels.resize(width * height); 53 | } 54 | ImageRGB(int x, int y, const std::vector& arr) :width(x), height(y) { 55 | pixels.resize(width * height); 56 | 57 | for (size_t i = 0; i < width * height; ++i) { 58 | pixels[i] = arr[i]; 59 | } 60 | } 61 | 62 | ImageRGB(const ImageRGB& img) { 63 | width = img.width; 64 | height = img.height; 65 | pixels.resize(img.width * img.height); 66 | 67 | for (size_t i = 0; i < width * height; ++i) { 68 | pixels[i] = img.pixels[i]; 69 | } 70 | } 71 | 72 | ImageRGB& operator=(const ImageRGB& img) { 73 | width = img.width; 74 | height = img.height; 75 | pixels.resize(img.width * img.height); 76 | 77 | for (size_t i = 0; i < width * height; ++i) { 78 | pixels[i] = img.pixels[i]; 79 | } 80 | 81 | return *this; 82 | } 83 | 84 | bool load(const std::string& name, bool need_normalize = true) { 85 | 86 | cimg_library::CImg image(name.c_str()); 87 | 88 | width = image.width(); 89 | height = image.height(); 90 | pixels.resize(width * height); 91 | 92 | for (int i = 0; i < width; i++) { 93 | for (int j = 0; j < height; j++) { 94 | Float r = image(i, j, 0, 0); 95 | Float g = image(i, j, 0, 1); 96 | Float b = image(i, j, 0, 2); 97 | if (need_normalize) { 98 | Set(i, j, Color(r, g, b) * (1.0/255)); 99 | } 100 | else { 101 | Set(i, j, Color(r, g, b)); 102 | } 103 | } 104 | } 105 | return true; 106 | } 107 | void save(const std::string& name, Float inv_gamma = 1.0 / 2.2) { 108 | cimg_library::CImg image(width, height, 1, 3); 109 | for (int i = 0; i < width; i++) { 110 | for (int j = 0; j < height; j++) { 111 | Color c = At(i, j); 112 | c = Clamp(c, 0, 1); 113 | c = Color(std::pow(c.r, inv_gamma), std::pow(c.g, inv_gamma), std::pow(c.b, inv_gamma)); 114 | c = c * 255; 115 | for (int k = 0; k < 3; ++k) { 116 | image(i, j, 0, k) = c[k]; 117 | } 118 | } 119 | } 120 | try { 121 | image.save(name.c_str()); 122 | } 123 | catch (cimg_library::CImgIOException) { 124 | printf("IO Exception from CImg, ignored."); 125 | } 126 | } 127 | 128 | Color At(int x, int y) const { 129 | int idx = y * width + x; 130 | return pixels[idx]; 131 | } 132 | Color At(Float x, Float y) const { 133 | int ix = (int)x; 134 | int iy = (int)y; 135 | 136 | Float dx = x - ix; 137 | Float dy = y - iy; 138 | 139 | Color c00 = At(ix, iy); 140 | Color c10 = At(ix + 1, iy); 141 | Color c01 = At(ix, iy + 1); 142 | Color c11 = At(ix + 1, iy + 1); 143 | 144 | Color c0 = c00 + (c10 - c00) * dx; 145 | Color c1 = c01 + (c11 - c01) * dx; 146 | return c0 + (c1 - c0) * dy; 147 | } 148 | 149 | 150 | bool Set(int x, int y, const Color& v) { 151 | int idx = y * width + x; 152 | if (idx >= pixels.size()) return false; 153 | 154 | pixels[y * width + x] = v; 155 | return true; 156 | } 157 | 158 | bool Set(int x, int y, const std::vector& v) { 159 | Color c(0, 0, 0); 160 | if (v.size() == 1) { 161 | c = Color(v[0], v[0], v[0]); 162 | } 163 | else if (v.size() == 2) { 164 | c = Color(v[0], v[1], 0.0f); 165 | } 166 | else if (v.size() >= 3) { 167 | c = Color(v[0], v[1], v[2]); 168 | } 169 | return Set(x, y, c); 170 | } 171 | 172 | int GetWidth() const { return width; } 173 | int GetHeight() const { return height; } 174 | private: 175 | int width, height; 176 | std::vector pixels; 177 | }; 178 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include "generate_blue_noise.h" 2 | 3 | int main(int argc, char** argv) { 4 | int x_resolution = 32, y_resolution = 32; 5 | int depth = 1; 6 | int max_iters = 4000; 7 | int kernel_size = -1; 8 | bool verbose = true; 9 | 10 | BlueNoiseGenerator generator(x_resolution, y_resolution, depth, kernel_size); 11 | generator.optimize(max_iters, verbose); 12 | 13 | return 0; 14 | } 15 | -------------------------------------------------------------------------------- /src/py_wrapper.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "generate_blue_noise.h" 3 | #include 4 | 5 | namespace py = pybind11; 6 | 7 | PYBIND11_MODULE(PyBlueNoiseGenerator, m) { 8 | m.doc() = "Generate blue noise texture"; 9 | 10 | py::class_(m, "BlueNoiseGenerator") 11 | .def(py::init()) 12 | .def(py::init(), 13 | py::arg("x_resolution"), 14 | py::arg("y_resolution"), 15 | py::arg("depth")=1, 16 | py::arg("kernel_size")=-1) 17 | .def("optimize", py::overload_cast(&BlueNoiseGenerator::optimize)) 18 | 19 | .def_readwrite("blue_noise_texture", &BlueNoiseGenerator::blue_noise) 20 | .def_readwrite("white_noise_texture", &BlueNoiseGenerator::white_noise) 21 | .def_readwrite("x_resolution", &BlueNoiseGenerator::x_resolution) 22 | .def_readwrite("y_resolution", &BlueNoiseGenerator::y_resolution) 23 | .def_readwrite("depth", &BlueNoiseGenerator::depth) 24 | .def_readwrite("kernel_size", &BlueNoiseGenerator::kernel_size) 25 | .def_readwrite("threads", &BlueNoiseGenerator::threads) 26 | 27 | ; 28 | } 29 | 30 | --------------------------------------------------------------------------------