├── .github └── workflows │ └── ccpp.yml ├── .gitignore ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── CMakeLists.txt ├── LICENSE ├── README.md ├── build.bat ├── build.sh ├── build_opencv.bat ├── build_opencv.sh ├── cmd ├── CMakeLists.txt └── Main.cpp ├── conanfile.txt ├── conanfile_opencv.txt ├── configure.sh ├── configure_opencv.sh └── lib ├── CMakeLists.txt ├── GaussianBlur.cpp ├── GaussianBlur.h ├── ImageAlgo.h ├── ImageAlgoBase.h ├── ImageAlgo_gil.h ├── ImageAlgo_opencv.h ├── VignettingCorrection.cpp ├── VignettingCorrection.h ├── vgncorr.cpp └── vgncorr.h /.github/workflows/ccpp.yml: -------------------------------------------------------------------------------- 1 | name: C/C++ CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: configure 17 | run: ./configure.sh 18 | - name: build 19 | run: ./build.sh 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | *.d 3 | 4 | # Compiled Object files 5 | *.slo 6 | *.lo 7 | *.o 8 | *.obj 9 | 10 | # Precompiled Headers 11 | *.gch 12 | *.pch 13 | 14 | # Compiled Dynamic libraries 15 | *.so 16 | *.dylib 17 | *.dll 18 | 19 | # Fortran module files 20 | *.mod 21 | *.smod 22 | 23 | # Compiled Static libraries 24 | *.lai 25 | *.la 26 | *.a 27 | *.lib 28 | 29 | # Executables 30 | *.exe 31 | *.out 32 | *.app 33 | 34 | # Build directory 35 | build*/* 36 | bin/* 37 | out/* 38 | bin/* 39 | 40 | # VS directories 41 | .vs/* 42 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "(gdb) Launch", 6 | "type": "cppdbg", 7 | "request": "launch", 8 | // Resolved by CMake Tools: 9 | "program": "${command:cmake.launchTargetPath}", 10 | "args": ["nature.jpg"], 11 | "stopAtEntry": false, 12 | "cwd": "${workspaceFolder}", 13 | "environment": [ 14 | { 15 | // add the directory where our target was built to the PATHs 16 | // it gets resolved by CMake Tools: 17 | "name": "PATH", 18 | "value": "$PATH:${command:cmake.launchTargetDirectory}" 19 | }, 20 | { 21 | "name": "OTHER_VALUE", 22 | "value": "Something something" 23 | } 24 | ], 25 | "externalConsole": true, 26 | "MIMode": "gdb", 27 | "setupCommands": [ 28 | { 29 | "description": "Enable pretty-printing for gdb", 30 | "text": "-enable-pretty-printing", 31 | "ignoreFailures": true 32 | } 33 | ] 34 | } 35 | ] 36 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "C_Cpp.default.configurationProvider": "ms-vscode.cmake-tools" 3 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "tasks": [ 3 | { 4 | "type": "shell", 5 | "label": "C/C++: g++ build active file", 6 | "command": "/usr/bin/g++", 7 | "args": [ 8 | "-g", 9 | "${file}", 10 | "-o", 11 | "${fileDirname}/${fileBasenameNoExtension}" 12 | ], 13 | "options": { 14 | "cwd": "${workspaceFolder}" 15 | }, 16 | "problemMatcher": [ 17 | "$gcc" 18 | ], 19 | "group": { 20 | "kind": "build", 21 | "isDefault": true 22 | } 23 | } 24 | ], 25 | "version": "2.0.0" 26 | } -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Configure: 2 | # Set environment variable FETCHCONTENT_BASE_DIR to the desired download location for external libraries and headers 3 | # Otherwise ~/.cmake-deps will be used if user directory is found - otherwise project/_deps is used. 4 | 5 | cmake_minimum_required(VERSION 3.12) 6 | 7 | project( vgncorr ) 8 | 9 | set(CMAKE_CXX_STANDARD 17) 10 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 11 | 12 | add_subdirectory (lib) 13 | add_subdirectory (cmd) 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Michael Schulte 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 | # Vignetting Correction 2 | 3 | Automatic Vignetting Correction algorithm in C++ after Laura Lopez-Fuentes's work 4 | [Revisiting Image Vignetting Correction by Constrained Minimization of log-Intensity Entropy](https://www.researchgate.net/publication/300786398_Revisiting_Image_Vignetting_Correction_by_Constrained_Minimization_of_Log-Intensity_Entropy) which is based on the paper [Single-Image Vignetting Correction by Constrained Minimization of log-Intensity Entropy](https://www.semanticscholar.org/paper/Single-Image-Vignetting-Correction-by-Constrained-Torsten/e355fffc31fa0a7c5309bd2b90da84810e5ffb70) 5 | 6 | See also 7 | * https://github.com/HJCYFY/Vignetting-Correction 8 | * https://github.com/dajuric/dot-devignetting 9 | 10 | ## Build the project 11 | 12 | ### Prerequisites 13 | 14 | Install: 15 | - [Python](https://python.org) 16 | - [Conan](https://conan.io) 17 | - [CMake](https://cmake.org) 18 | 19 | On Ubuntu: 20 | ```shell 21 | ./configure.sh 22 | ./build.sh 23 | ./bin/vgncorr 24 | ``` 25 | 26 | On Windows: 27 | ```shell 28 | build.bat 29 | bin\vgncorr.exe 30 | ``` 31 | 32 | If you want to use opencv library instead of the default boost gil, then execute the corresponding `configure_opencv` and `build_opencv` files. 33 | 34 | The resulting image is copied to the same folder. 35 | 36 | -------------------------------------------------------------------------------- /build.bat: -------------------------------------------------------------------------------- 1 | if not exist build mkdir build 2 | if not exist bin mkdir bin 3 | cd build 4 | set USE_OPENCV= 5 | conan install .. --settings arch=x86_64 6 | cmake .. -DCMAKE_GENERATOR_PLATFORM=x64 7 | cmake --build . --config Release 8 | copy cmd\bin\vgncorr_cmd.exe ..\bin\vgncorr.exe 9 | cd .. 10 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | mkdir -p build 2 | mkdir -p bin 3 | cd build 4 | cmake .. 5 | cmake --build . --config Debug 6 | cp cmd/bin/vgncorr_cmd ../bin/vgncorr 7 | cd .. 8 | -------------------------------------------------------------------------------- /build_opencv.bat: -------------------------------------------------------------------------------- 1 | if not exist build mkdir build 2 | if not exist bin mkdir bin 3 | cd build 4 | set USE_OPENCV=1 5 | conan install ..\conanfile_opencv.txt --settings arch=x86_64 6 | cmake -E env CXXFLAGS="-DUSE_OPENCV" cmake .. -DCMAKE_GENERATOR_PLATFORM=x64 7 | cmake --build . --config Release 8 | 9 | copy cmd\bin\vgncorr_cmd.exe ..\bin\vgncorr.exe 10 | cd .. 11 | -------------------------------------------------------------------------------- /build_opencv.sh: -------------------------------------------------------------------------------- 1 | mkdir -p build 2 | mkdir -p bin 3 | cd build 4 | 5 | cmake -E env CXXFLAGS="-DUSE_OPENCV" cmake .. 6 | 7 | cmake --build . --config Release 8 | 9 | cp cmd/bin/vgncorr_cmd ../bin/vgncorr 10 | cd .. 11 | -------------------------------------------------------------------------------- /cmd/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.12) 2 | 3 | project( vgncorr_cmd ) 4 | 5 | set(CMAKE_CXX_STANDARD 17) 6 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 7 | 8 | include(${CMAKE_BINARY_DIR}/conanbuildinfo.cmake) 9 | conan_basic_setup() 10 | 11 | set ( SOURCE_FILES 12 | Main.cpp ) 13 | 14 | add_executable( vgncorr_cmd ${SOURCE_FILES} ) 15 | 16 | target_link_libraries( vgncorr_cmd LINK_PUBLIC vgncorr_lib ${CONAN_LIBS}) 17 | -------------------------------------------------------------------------------- /cmd/Main.cpp: -------------------------------------------------------------------------------- 1 | #include "../lib/vgncorr.h" 2 | 3 | #include 4 | 5 | int main(int argc, char *argv[]) { 6 | using namespace vgncorr; 7 | if (argc < 2) { 8 | std::cerr << "Please provide a filename\n"; 9 | return -1; 10 | } 11 | std::string const path = argv[1]; 12 | 13 | struct stat buf; 14 | if (stat(path.c_str(), &buf) != 0) { 15 | std::cerr << "File not found!\n"; 16 | return -1; 17 | } 18 | auto out_path = path; 19 | out_path = out_path.replace(path.find("."), 1, "_corr."); 20 | 21 | vgncorr::Config config {}; 22 | vgncorr::correct(config, path, out_path); 23 | 24 | return 0; 25 | } 26 | -------------------------------------------------------------------------------- /conanfile.txt: -------------------------------------------------------------------------------- 1 | [requires] 2 | libpng/1.6.37 3 | libjpeg/9d 4 | boost/1.72.0 5 | 6 | [generators] 7 | cmake 8 | -------------------------------------------------------------------------------- /conanfile_opencv.txt: -------------------------------------------------------------------------------- 1 | [requires] 2 | opencv/4.1.1@conan/stable 3 | 4 | [generators] 5 | cmake 6 | -------------------------------------------------------------------------------- /configure.sh: -------------------------------------------------------------------------------- 1 | pip3 install setuptools 2 | pip3 install --user conan 3 | source ~/.profile 4 | mkdir -p build 5 | cd build 6 | conan install .. 7 | # sudo apt install cmake 8 | -------------------------------------------------------------------------------- /configure_opencv.sh: -------------------------------------------------------------------------------- 1 | pip3 install setuptools 2 | pip3 install --user conan 3 | source ~/.profile 4 | mkdir -p build 5 | cd build 6 | conan install ../conanfile_opencv.txt --build=opencv --build=protobuf 7 | #conan install ../conanfile_opencv.txt 8 | # sudo apt install cmake 9 | -------------------------------------------------------------------------------- /lib/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | 2 | cmake_minimum_required(VERSION 3.12) 3 | 4 | project( vgncorr_lib ) 5 | 6 | set(CMAKE_CXX_STANDARD 17) 7 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 8 | 9 | # include directories retrieved from conan 10 | include(${CMAKE_BINARY_DIR}/conanbuildinfo.cmake) 11 | conan_basic_setup() 12 | 13 | set ( SOURCE_FILES 14 | VignettingCorrection.cpp 15 | VignettingCorrection.h 16 | GaussianBlur.h 17 | GaussianBlur.cpp 18 | ImageAlgo.h 19 | ImageAlgoBase.h 20 | ImageAlgo_gil.h 21 | ImageAlgo_opencv.h 22 | vgncorr.h 23 | vgncorr.cpp 24 | ) 25 | 26 | add_library( vgncorr_lib ${SOURCE_FILES} ) 27 | if(DEFINED ENV{USE_OPENCV}) 28 | add_compile_definitions("USE_OPENCV") 29 | endif() 30 | target_include_directories( vgncorr_lib PUBLIC "${boost_SOURCE_DIR}/include" ) 31 | -------------------------------------------------------------------------------- /lib/GaussianBlur.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include "GaussianBlur.h" 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | using namespace imgalg; 9 | using namespace std; 10 | 11 | // gaussian blur algorithm by Ivan Kutskir 12 | // http://blog.ivank.net/fastest-gaussian-blur.html 13 | 14 | static vector boxesForGauss(int sigma, 15 | int n) // standard deviation, number of boxes 16 | { 17 | auto const wIdeal = 18 | sqrt((12 * sigma * sigma / n) + 1); // Ideal averaging filter width 19 | int wl = static_cast(wIdeal); 20 | if (wl % 2 == 0) --wl; 21 | int wu = wl + 2; 22 | 23 | auto mIdeal = 24 | (12 * sigma * sigma - n * wl * wl - 4 * n * wl - 3 * n) / (-4 * wl - 4); 25 | int m = iround(mIdeal); 26 | 27 | vector sizes(n); 28 | for (auto i = 0; i < n; i++) { 29 | sizes[i] = i < m ? wl : wu; 30 | } 31 | return sizes; 32 | } 33 | 34 | static void boxBlurH(PixelT const* scl, PixelT* tcl, int w, 35 | int h, int r) { 36 | float const iarr = 1.f / (2 * r + 1); 37 | for (auto i = 0; i < h; i++) { 38 | auto ti = i * w, li = ti, ri = ti + r; 39 | auto const fv = scl[ti], lv = scl[ti + w - 1]; 40 | auto val = (r + 1) * fv; 41 | for (auto j = 0; j < r; ++j) { 42 | val += scl[ti + j]; 43 | } 44 | for (auto j = 0; j <= r; ++j) { 45 | val += scl[ri++] - fv; 46 | tcl[ti++] = clamp(val * iarr); 47 | } 48 | for (auto j = r + 1; j < w - r; ++j) { 49 | val += scl[ri++] - scl[li++]; 50 | tcl[ti++] = clamp(val * iarr); 51 | } 52 | for (auto j = w - r; j < w; ++j) { 53 | val += lv - scl[li++]; 54 | tcl[ti++] = clamp(val * iarr); 55 | } 56 | } 57 | } 58 | 59 | static void boxBlurT(PixelT const* scl, PixelT* tcl, int w, 60 | int h, int r) { 61 | float const iarr = 1.f / (2 * r + 1); 62 | for (auto i = 0; i < w; i++) { 63 | auto ti = i, li = ti, ri = ti + r * w; 64 | auto const fv = scl[ti], lv = scl[ti + w * (h - 1)]; 65 | auto val = (r + 1) * fv; 66 | for (auto j = 0; j < r; ++j) { 67 | val += scl[ti + j * w]; 68 | } 69 | for (auto j = 0; j <= r; ++j) { 70 | val += scl[ri] - fv; 71 | tcl[ti] = clamp(val * iarr); 72 | ri += w; 73 | ti += w; 74 | } 75 | for (auto j = r + 1; j < h - r; ++j) { 76 | val += scl[ri] - scl[li]; 77 | tcl[ti] = clamp(val * iarr); 78 | li += w; 79 | ri += w; 80 | ti += w; 81 | } 82 | for (auto j = h - r; j < h; ++j) { 83 | val += lv - scl[li]; 84 | tcl[ti] = clamp(val * iarr); 85 | li += w; 86 | ti += w; 87 | } 88 | } 89 | } 90 | 91 | static void boxBlur(PixelT* scl, PixelT* tcl, int w, int h, int r) { 92 | memcpy(tcl, scl, w * h * sizeof(PixelT)); 93 | boxBlurH(tcl, scl, w, h, r); 94 | boxBlurT(scl, tcl, w, h, r); 95 | } 96 | 97 | vector gaussBlur(vector const& source, int w, int h, int radius, 98 | int n) { 99 | auto scl = source; 100 | auto tcl = vector(source.size()); 101 | auto *pscl = &scl[0]; 102 | auto *ptcl = &tcl[0]; 103 | 104 | auto bxs = boxesForGauss(radius, n); 105 | boxBlur(pscl, ptcl, w, h, (bxs[0] - 1) / 2); 106 | boxBlur(ptcl, pscl, w, h, (bxs[1] - 1) / 2); 107 | boxBlur(pscl, ptcl, w, h, (bxs[2] - 1) / 2); 108 | 109 | return tcl; 110 | } 111 | 112 | Img GaussianBlur::blur(Img const& img, int radius, int n) { 113 | auto pixels = std::vector(); 114 | auto const [cols, rows] = dimensions(img); 115 | auto const height = static_cast(rows); 116 | auto const width = static_cast(cols); 117 | 118 | pixels.reserve(width * height); 119 | 120 | reduce( 121 | pixels, const_view(img), [](int const row) {}, 122 | [](auto& p, auto const row_it, int const col) { 123 | p.push_back(row_it[col]); 124 | }); 125 | 126 | struct Param { 127 | std::vector blurred; 128 | int idx; 129 | }; 130 | Param p = {gaussBlur(pixels, width, height, radius, n), 0}; 131 | 132 | Img result = img; 133 | 134 | reduce( 135 | p, view(result), [](int const row) {}, 136 | [](auto &p, auto &row_it, int const col) { 137 | #ifdef USE_OPENCV 138 | row_it[col] = p.blurred[p.idx++]; 139 | #else 140 | const_cast(row_it[col][0]) = p.blurred[p.idx++]; 141 | #endif 142 | }); 143 | return result; 144 | } 145 | -------------------------------------------------------------------------------- /lib/GaussianBlur.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "ImageAlgo.h" 4 | 5 | class GaussianBlur : public imgalg::ImageAlgo { 6 | public: 7 | static imgalg::Img blur(imgalg::Img const& src, int radius, int n = 3); 8 | }; 9 | -------------------------------------------------------------------------------- /lib/ImageAlgo.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "ImageAlgoBase.h" 9 | 10 | namespace imgalg { 11 | 12 | constexpr static PixelT clamp(int const i) { 13 | return static_cast(((0xff - i) >> 31) | (i & ~(i >> 31))); 14 | } 15 | 16 | constexpr static PixelT clamp(Real const f) { return clamp(iround(f)); } 17 | 18 | class ImageAlgo : public ImageAlgoBase { 19 | public: 20 | using HistogramType = Real; 21 | 22 | template static constexpr T square(T const x) { return x * x; } 23 | 24 | template static Real sqrt(T sq) { 25 | return sqrtf(static_cast(sq)); 26 | } 27 | 28 | static Real dist(Point const &p) { return sqrt(square(p.x) + square(p.y)); } 29 | 30 | template 31 | static bool reduce(T &akku, View &view, 32 | std::function const &row_fun, Fun const &fun) { 33 | auto const [cols, rows] = dimensions(view); 34 | 35 | using PixelIt = decltype(row_begin(view, 0)); 36 | static auto constexpr bool_result = 37 | std::is_same, 38 | bool>::value; 39 | 40 | for (int row = 0; row < rows; ++row) { 41 | auto row_it = row_begin(view, row); 42 | row_fun(row); 43 | for (int col = 0; col < cols; ++col) { 44 | if constexpr (bool_result) { 45 | if (not fun(akku, row_it, col)) { 46 | return false; 47 | } 48 | } else { 49 | fun(akku, row_it, col); 50 | } 51 | } 52 | } 53 | 54 | return true; 55 | } 56 | }; 57 | } // namespace imgalg 58 | -------------------------------------------------------------------------------- /lib/ImageAlgoBase.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace imgalg { 4 | using Real = float; 5 | 6 | constexpr static int iround(Real const f) { 7 | // only works for positive f 8 | return static_cast(f + 0.5f); 9 | } 10 | 11 | constexpr static int iround(int const i) { return i; } 12 | 13 | template static constexpr int div_round(T const a, T const b) { 14 | return static_cast((a + b / 2) / b); 15 | } 16 | } // namespace imgalg 17 | 18 | #ifdef USE_OPENCV 19 | #include "ImageAlgo_opencv.h" 20 | #else 21 | #include "ImageAlgo_gil.h" 22 | #endif -------------------------------------------------------------------------------- /lib/ImageAlgo_gil.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | 6 | #ifdef _MSC_VER 7 | #pragma warning(push) 8 | #pragma warning(disable : 4996) // suppress warning to use wcstomb_s in 9 | #pragma warning(disable : 4244) 10 | #endif 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #ifdef _MSC_VER 18 | #pragma warning(pop) 19 | #endif 20 | 21 | namespace imgalg { 22 | using Pixel = boost::gil::gray8_pixel_t; 23 | using PixelT = uint8_t; 24 | using PixelOrig = boost::gil::rgb8_pixel_t; 25 | using PixelOut = PixelOrig; 26 | using Img = boost::gil::image; 27 | using ImgOrig = boost::gil::image; 28 | using ImgView = boost::gil::image::const_view_t; 29 | using ImgViewOrig = boost::gil::image::const_view_t; 30 | using Point = ImgView::point_t; 31 | 32 | using Size = Point; 33 | 34 | class ImageAlgoBase { 35 | public: 36 | template 37 | static Size dimensions(V const &v) { 38 | return v.dimensions(); 39 | } 40 | template 41 | static int num_channels(V const &v) { 42 | return boost::gil::num_channels::value; 43 | } 44 | 45 | static Size create_img(Size const &size) { return size; } 46 | 47 | template 48 | static P *row_begin(V &img, int const row) { 49 | return img.row_begin(row); 50 | } 51 | 52 | template 53 | static P const *row_begin(V const &img, int const row) { 54 | return img.row_begin(row); 55 | } 56 | 57 | static Img scaled_down_gray(ImgViewOrig const &input_image, int const SF) { 58 | auto const &orig_dim = input_image.dimensions(); 59 | auto const dim = Point{orig_dim.x / SF, orig_dim.y / SF}; 60 | Img result(dim); 61 | Img gray(orig_dim); 62 | auto v = view(result); 63 | auto g = view(gray); 64 | copy_pixels(::boost::gil::color_converted_view(input_image), g); 65 | auto const [cols, rows] = dim; 66 | auto const SF2 = SF * SF; 67 | for (int row = 0; row < rows; ++row) { 68 | auto *out = row_begin(v, row); 69 | auto block_sums = std::vector(cols); 70 | // cache-friendly optimization taking the whole rows 71 | for (int y = 0; y < SF; ++y) { 72 | auto const *buf = g.row_begin(row * SF + y); 73 | for (auto& sum: block_sums) { 74 | sum += std::accumulate(buf, &buf[SF], 0); 75 | buf += SF; 76 | } 77 | } 78 | auto const average = [=](auto const &sum) { return div_round(sum, SF2); }; 79 | std::transform(block_sums.begin(), block_sums.end(), out, average); 80 | } 81 | return result; 82 | } 83 | 84 | template 85 | static void img_action(std::string const &path, Fun const &fun) { 86 | auto const lower = [](auto const& s) { 87 | std::string data = s; 88 | std::transform(data.cbegin(), data.cend(), data.begin(), 89 | [](unsigned char c) { return std::tolower(c); }); 90 | return data; 91 | }; 92 | auto const has_ending = [&](std::string const &ending) -> bool { 93 | return path.length() >= ending.length() and 94 | not lower(path).compare(path.length() - ending.length(), ending.length(), 95 | ending); 96 | }; 97 | if (has_ending(".png")) { 98 | fun(boost::gil::png_tag()); 99 | } else if (has_ending(".jpg") or has_ending(".jpeg")) { 100 | fun(boost::gil::jpeg_tag()); 101 | } else if (has_ending(".bmp")) { 102 | fun(boost::gil::bmp_tag()); 103 | } else { 104 | throw std::runtime_error(std::string("Unrecognized file type in ") + 105 | path); 106 | } 107 | } 108 | 109 | template 110 | static void load_image(I &orig, std::string const &path) { 111 | img_action(path, 112 | [&](auto tag) { boost::gil::read_image(path, orig, tag); }); 113 | } 114 | 115 | template 116 | static void save_image(I const &img, std::string const &path) { 117 | img_action(path, [&](auto tag) { 118 | boost::gil::write_view(path, const_view(img), tag); 119 | }); 120 | } 121 | }; 122 | 123 | } // namespace imgalg 124 | -------------------------------------------------------------------------------- /lib/ImageAlgo_opencv.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | namespace imgalg { 9 | 10 | using Point = cv::Point2i; 11 | using ImgOrig = cv::Mat; 12 | using ImgViewOrig = cv::Mat; 13 | using Img = ImgOrig; 14 | using Pixel = uint8_t; 15 | using PixelOrig = uint8_t; 16 | using PixelOut = cv::Vec3b; 17 | using PixelT = uint8_t; 18 | using Size = cv::Size; 19 | 20 | class ImageAlgoBase { 21 | public: 22 | static inline cv::Mat const &const_view(cv::Mat const &mat) { return mat; } 23 | static inline cv::Mat &view(cv::Mat &mat) { return mat; } 24 | static Size dimensions(cv::Mat const &mat) { return mat.size(); } 25 | static int num_channels(cv::Mat const& mat) { return mat.type() == CV_8UC3 ? 3 : 1; } 26 | static Img create_img(Size const &size) { return cv::Mat(size, CV_8UC3); } 27 | static Img scaled_down_gray(ImgViewOrig const &input_image, int const SF) { 28 | Img result; 29 | cv::resize(input_image, result, cv::Size(), 1. / SF, 1. / SF, 30 | cv::INTER_LINEAR_EXACT); 31 | cv::Mat gray(result.size(), CV_8UC1); 32 | cv::cvtColor(result, gray, cv::COLOR_BGR2GRAY); 33 | return gray; 34 | } 35 | template 36 | static P *row_begin(V &img, int const row) { 37 | return img.ptr

(row); 38 | } 39 | 40 | template 41 | static P const *row_begin(V const &img, int const row) { 42 | return img.ptr

(row); 43 | } 44 | 45 | static void load_image(ImgOrig &orig, cv::String const &path) { 46 | orig = cv::imread(path, cv::IMREAD_UNCHANGED); 47 | } 48 | 49 | static bool save_image(ImgOrig const &img, cv::String const &path) { 50 | return cv::imwrite(path, img); 51 | } 52 | }; 53 | } // namespace imgalg 54 | -------------------------------------------------------------------------------- /lib/VignettingCorrection.cpp: -------------------------------------------------------------------------------- 1 | #include "VignettingCorrection.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include "GaussianBlur.h" 11 | 12 | namespace vgncorr { 13 | 14 | using namespace imgalg; 15 | 16 | static auto constexpr DebugPrint = false; 17 | static auto constexpr MeasureTime = true; 18 | 19 | constexpr float log2f(float val) { 20 | union { 21 | float val; 22 | int32_t x; 23 | } u = {val}; 24 | auto const log_2i = (((u.x >> 23) & 255) - 128); 25 | u.x = (u.x & ~(255 << 23)) + (127 << 23); 26 | return log_2i + ((-0.34484843f) * u.val + 2.02466578f) * u.val - 0.67487759f; 27 | } 28 | 29 | constexpr int log2i(int const val) { 30 | if (val == 1) { 31 | return 0; 32 | } 33 | return 1 + log2i(val / 2); 34 | } 35 | 36 | /// Polynomial to calculate the vignetting correction with. 37 | /// This is of the form: 38 | /// g_a,b,c(r) = 1 + ar^2 + br^4 + cr^6. 39 | class Poly : public imgalg::ImageAlgo { 40 | public: 41 | static auto constexpr num_coefficients = 3; 42 | using CoeffType = Real; 43 | using Coefficients = std::array; 44 | 45 | Poly(Coefficients const &coefficients, Point const &mid_point) 46 | : coeffs_(coefficients), 47 | mid_point_(mid_point), 48 | d2_(square(dist(mid_point))) {} 49 | 50 | void set_row(int const row) const; 51 | /// Check if the polynomial is increasing. I.e. the derivative is positive for 52 | /// the current coefficients. 53 | bool is_increasing() const; 54 | Real calc_at(int const col) const; 55 | 56 | private: 57 | Coefficients const coeffs_; 58 | Point const mid_point_; 59 | Real const d2_; 60 | Real mutable sq_row_dist_{}; 61 | }; 62 | 63 | void Poly::set_row(int const row) const { 64 | sq_row_dist_ = static_cast(square(mid_point_.y - row)); 65 | } 66 | 67 | Real Poly::calc_at(int const col) const { 68 | auto const r2 = (sq_row_dist_ + square(mid_point_.x - col)) / d2_; 69 | auto const r4 = r2 * r2; 70 | auto const r6 = r4 * r2; 71 | auto const g = 1 + coeffs_[0] * r2 + coeffs_[1] * r4 + coeffs_[2] * r6; 72 | return g; 73 | } 74 | 75 | /** 76 | The derivate of the given polynomial 77 | g = g_a,b,c(r) = 1 + ar^2 + br^4 + cr^6 is: 78 | g'_a,b,c(r) = 2*ar + 4*br^3 + 6*cr^5 has to be > 0, 79 | so we get - replacing q = r^2 and dividing by 2r: 80 | 81 | a + 2*b*q + 3*c*q^2 > 0 82 | 83 | defining: 84 | 85 | d = 3 * a * c 86 | b2 = b * b 87 | 88 | q_plus = (-2*b + sqrt(4*b^2 - 12*ac)) / (6*c) # d = 3ac 89 | q_minus = (-2*b - sqrt(4*b^2 - 12*ac)) / (6*c) 90 | => 91 | q_plus = (-b + sqrt(b - d)) / (3*c) 92 | q_minus = (-b - sqrt(b - d)) / (3*c) 93 | 94 | Any of the Conditions C1 - C9 must hold true for the derivative to be positive: 95 | 96 | [Horizontal positive line] 97 | C1 = (a > 0 and b == c == 0); 98 | [Increasing line with non-positive root] 99 | C2 = (a >= 0 and b > 0 and c = 0); 100 | [Decreasing line with root >= 1] 101 | C3 = (c = 0 and b < 0 and -a <= 2b); 102 | [Convex parab. without roots] 103 | C4 = (c > 0 and b2 < d); 104 | [Convex parab., only one non-positive root] 105 | C5 = (c > 0 and b2 = d and b >= 0); 106 | [Convex parab., only one root >= 1] 107 | C6 = (c > 0 and b2 = d and -b >= 3c); 108 | [Convex parab., non-positive highest root] 109 | C7 = (c > 0 and b2 > d and q_plus <= 0); 110 | [Convex parab., lowest root >= 1] 111 | C8 = (c > 0 and b2 > d and q_minus >= 1); 112 | [Concave parab., lowest root <= 0; highest root >= 1] 113 | C9 = (c < 0 and b2 > d and q_plus >= 1 and q_minus <= 0); 114 | */ 115 | bool Poly::is_increasing() const { 116 | auto const [a, b, c] = coeffs_; 117 | 118 | if ((a > 0 and b == 0 and c == 0) or // C1 119 | (a >= 0 and b > 0 and c == 0)) { // C2 120 | return false; 121 | } 122 | auto const d = 3 * a * c; 123 | auto const b2 = b * b; 124 | if ((c > 0 and b2 < d) or // C4 125 | (c > 0 and b2 == d and b >= 0) or // C5 126 | (c > 0 and b2 == d and -b >= 3 * c)) { // C6 127 | return true; 128 | } 129 | if (c == 0) { 130 | return b < 0 and -a <= 2 * b; // shortcut + C3 131 | } 132 | auto const e = b2 - d; 133 | if (e <= 0) { 134 | return false; 135 | } 136 | // b2 > d ensured (with e > 0) 137 | auto const sqrt_e = sqrt(e); 138 | auto const c_3 = 3 * c; 139 | auto const q_plus = (-b + sqrt_e) / c_3; 140 | if (c > 0 and q_plus <= 0) { // C7 141 | return true; 142 | } 143 | auto const q_minus = (-b - sqrt_e) / c_3; 144 | if (c > 0 and q_minus >= 1) { // C8 145 | return true; 146 | } 147 | if (c < 0 and q_plus >= 1 and q_minus <= 0) { // C9 148 | return true; 149 | } 150 | return false; 151 | } 152 | 153 | VignettingCorrection::VignettingCorrection(ImgOrig const &input_image, Config const& config) 154 | : config_(default_config(input_image, config)), 155 | input_image_orig_(const_view(input_image)), 156 | input_image_( 157 | GaussianBlur::blur(scaled_down_gray(input_image_orig_, config_.scale), config_.blur)) { 158 | } 159 | 160 | VignettingCorrection::~VignettingCorrection() {} 161 | 162 | void VignettingCorrection::_smooth_histogram( 163 | HistogramType (&histogram)[HistogramSize + 1]) const { 164 | auto const smooth_radius = config_.histogram_smoothing; 165 | std::vector histo_orig(HistogramSize + 2 * smooth_radius, 0); 166 | 167 | // add mirrored values at the edges 168 | for (int i = 0; i < config_.histogram_smoothing; ++i) { 169 | // 0 -> 4, ... 3 -> 1 170 | histo_orig[i] = histogram[smooth_radius - i]; 171 | // 260 -> 254, 261 -> 253 ... 172 | histo_orig[HistogramSize + smooth_radius + i] = 173 | histogram[HistogramSize - 2 - i]; 174 | } 175 | memcpy(histo_orig.data() + smooth_radius, histogram, 176 | HistogramSize * sizeof(HistogramType)); 177 | 178 | auto const factor_sum = square(smooth_radius + 1); 179 | // smooth the histogram 180 | for (int i = 0; i < HistogramSize; ++i) { 181 | HistogramType sum = 0; 182 | auto *orig = &histo_orig[i]; 183 | for (int j = 0; j < smooth_radius; ++j) { 184 | sum += (j + 1) * orig[j]; 185 | } 186 | sum += (smooth_radius + 1) * orig[smooth_radius]; 187 | for (int j = 0; j < smooth_radius; ++j) { 188 | sum += (smooth_radius - j) * orig[smooth_radius + j + 1]; 189 | } 190 | histogram[i] = sum / factor_sum; 191 | } 192 | } 193 | 194 | Real VignettingCorrection::_calc_H(Poly const &poly) const { 195 | static auto constexpr fact = (Depth - 1) / static_cast(log2i(Depth)) / 196 | (MaxAllowedBrightness / 255.f); 197 | 198 | struct CalcH { 199 | CalcH() { memset(histogram, 0, sizeof(histogram)); } 200 | 201 | HistogramType histogram[MaxAllowedBrightness + 1]; 202 | bool ok{true}; 203 | } c{}; 204 | 205 | auto const calc_pixel = [&poly](CalcH &c, auto const row_it, 206 | int const col) -> bool { 207 | auto const g = poly.calc_at(col); 208 | 209 | auto const log_img = fact * log2f(1 + g * row_it[col]); 210 | if (auto const k_lower = 211 | static_cast(log_img); k_lower < MaxAllowedBrightness) { 212 | // add discrete histogram value for actual floating point 213 | // example: 86.1 is to 0.9 in 86 and to 0.1 in 87 214 | auto const k = static_cast(log_img) - k_lower; 215 | auto *hist = &c.histogram[k_lower]; 216 | hist[0] += 1 - k; 217 | hist[1] += k; 218 | return true; 219 | } 220 | return false; 221 | }; 222 | if (auto const result = reduce( 223 | c, const_view(input_image_), 224 | [&poly, this](int const row) { poly.set_row(row); }, calc_pixel); 225 | result) { 226 | return _calc_entropy(c.histogram); 227 | } else { 228 | return std::numeric_limits::max(); 229 | } 230 | } 231 | 232 | Real VignettingCorrection::_calc_entropy( 233 | HistogramType (&histogram)[MaxAllowedBrightness + 1]) const { 234 | _smooth_histogram(histogram); 235 | float sum = 0; 236 | for (int i = 0; i < MaxAllowedBrightness; ++i) { 237 | sum += histogram[i]; 238 | } 239 | Real H = 0; 240 | for (int i = 0; i < MaxAllowedBrightness; ++i) { 241 | if (auto const pk = histogram[i] / sum; pk) { 242 | // pk is < 1 (as divided by sum) => log < 0 243 | H -= pk * log2f(pk); 244 | } 245 | } 246 | return H; 247 | } 248 | 249 | Poly VignettingCorrection::_calc_best_poly() const { 250 | auto const mid = _center_of_mass(); 251 | Poly::Coefficients best_coefficients = {0, 0, 0}, current_best = {0, 0, 0}; 252 | auto H_min = _calc_H(Poly(best_coefficients, mid)); 253 | if constexpr (DebugPrint) { 254 | std::cout << "Hmin " << H_min << " @ (0, 0, 0) " << std::endl; 255 | } 256 | 257 | // check the new coefficients are minimizing the H value 258 | auto const chk_H = [&](int const coeff_idx, int const sign, float const delta) -> bool { 259 | auto coeff{best_coefficients}; 260 | coeff[coeff_idx] += sign * delta; 261 | auto const poly = Poly{coeff, mid}; 262 | auto found_min = false; 263 | 264 | if (poly.is_increasing()) { 265 | if (auto const H = _calc_H(poly); H < H_min) { 266 | current_best = coeff; 267 | H_min = H; 268 | if constexpr (DebugPrint) { 269 | std::cout << "Hmin " << H_min << " @ (" << coeff[0] << ", " << coeff[1] << ", " 270 | << coeff[2] << ")" << std::endl; 271 | } 272 | found_min = true; 273 | } 274 | } 275 | return found_min; 276 | }; 277 | 278 | auto walkback = 0; 279 | float delta = config_.delta_start_divider; 280 | auto const find_dir = [&]() { 281 | auto found_dir = 0; 282 | for (int idx : {0, 1, 2}) { 283 | for (int sign : {-1, 1}) { 284 | if (auto const dir = (idx + 1) * sign; 285 | dir != walkback and chk_H(idx, sign, delta)) { 286 | // dir != walkback => do not walk the same way back again 287 | found_dir = dir; 288 | } 289 | } 290 | } 291 | walkback = -found_dir; 292 | return found_dir; 293 | }; 294 | while (delta * config_.delta_max_precision >= 1) { 295 | auto found_dir = find_dir(); 296 | if (found_dir) { 297 | best_coefficients = current_best; 298 | } else { 299 | delta /= 2; 300 | } 301 | } 302 | 303 | return Poly{best_coefficients, {mid.x * config_.scale, mid.y * config_.scale}}; 304 | } 305 | 306 | Point VignettingCorrection::_center_of_mass() const { 307 | struct Params { 308 | float weight_x{0.f}; 309 | float weight_y{0.f}; 310 | float sum{0.f}; 311 | int row; 312 | } p{}; 313 | 314 | reduce( 315 | p, const_view(input_image_), [&p](int const row) { p.row = row; }, 316 | [](Params &p, auto const &row_it, int const col) { 317 | auto const d = row_it[col]; 318 | p.sum += d; 319 | p.weight_y += (p.row + 1) * d; 320 | p.weight_x += (col + 1) * d; 321 | }); 322 | auto const s2 = p.sum / 2; 323 | return {div_round(p.weight_x, p.sum), div_round(p.weight_y, p.sum)}; 324 | } 325 | 326 | ImgOrig VignettingCorrection::correct() { 327 | [[maybe_unused]] auto const start = std::chrono::high_resolution_clock::now(); 328 | auto const dim = dimensions(input_image_orig_); 329 | auto const[cols, rows] = dim; 330 | auto const best_poly = _calc_best_poly(); 331 | 332 | if constexpr (MeasureTime) { 333 | auto const end = std::chrono::high_resolution_clock::now(); 334 | 335 | auto const duration = 336 | std::chrono::duration_cast(end - start) 337 | .count(); 338 | std::cout << "took: " << duration << " ms" << std::endl; 339 | } 340 | ImgOrig result(create_img(dim)); 341 | 342 | auto v = view(result); 343 | const auto nc = num_channels(input_image_orig_); 344 | 345 | for (auto row = 0; row < rows; ++row) { 346 | auto row_it = row_begin(input_image_orig_, row); 347 | auto output_it = row_begin(v, row); 348 | best_poly.set_row(row); 349 | for (auto col = 0; col < cols; ++col) { 350 | auto const g = best_poly.calc_at(col); 351 | auto &pix_out = output_it[col]; 352 | auto const &pix_in = row_it[col]; 353 | for (int i = 0; i < nc; ++i) { 354 | pix_out[i] = clamp(g * pix_in[i]); 355 | } 356 | } 357 | } 358 | 359 | return result; 360 | } 361 | 362 | Config VignettingCorrection::default_config(imgalg::ImgOrig const &img, Config const& config) { 363 | auto const [cols, _] = dimensions(img); 364 | Config ret = config; 365 | 366 | if (ret.blur == 0 or ret.scale == 0) { 367 | ret.scale = cols < 2000 ? 8 : 16; 368 | ret.blur = std::max(5, static_cast(cols) / (ret.scale * 25)); 369 | std::cout << "scale = " << ret.scale << ", blur = " << ret.blur << "\n"; 370 | } 371 | return ret; 372 | } 373 | 374 | } // namespace vgncorr 375 | -------------------------------------------------------------------------------- /lib/VignettingCorrection.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "ImageAlgo.h" 4 | #include "vgncorr.h" 5 | 6 | namespace vgncorr { 7 | using Real = float; 8 | using HistogramType = Real; 9 | 10 | class Poly; 11 | 12 | class VignettingCorrection : public imgalg::ImageAlgo { 13 | public: 14 | VignettingCorrection(imgalg::ImgOrig const &img, Config const& config); 15 | ~VignettingCorrection(); 16 | 17 | static Config default_config(imgalg::ImgOrig const& img, Config const& config); 18 | 19 | imgalg::ImgOrig correct(); 20 | static auto constexpr Depth = 256; 21 | static auto constexpr MaxAllowedBrightness = 255; 22 | static auto constexpr HistogramSize = MaxAllowedBrightness; 23 | 24 | private: 25 | Real _calc_H(Poly const &poly) const; 26 | Real _calc_entropy( 27 | HistogramType (&histogram)[MaxAllowedBrightness + 1]) const; 28 | 29 | imgalg::Point _center_of_mass() const; 30 | Poly _calc_best_poly() const; 31 | void _smooth_histogram(HistogramType (&histogram)[HistogramSize + 1]) const; 32 | 33 | Config config_; 34 | imgalg::ImgViewOrig const input_image_orig_; 35 | // scaled and gray version of the input image 36 | imgalg::Img input_image_; 37 | }; 38 | } // namespace vgncorr 39 | -------------------------------------------------------------------------------- /lib/vgncorr.cpp: -------------------------------------------------------------------------------- 1 | #include "vgncorr.h" 2 | 3 | #include "VignettingCorrection.h" 4 | 5 | namespace vgncorr 6 | { 7 | using namespace imgalg; 8 | 9 | void correct(Config const& config, std::string const& filename_in, std::string const& filename_out) 10 | { 11 | ImgOrig orig; 12 | using namespace imgalg; 13 | 14 | ImageAlgo::load_image(orig, filename_in); 15 | 16 | vgncorr::VignettingCorrection corr(orig, config); 17 | auto const out = corr.correct(); 18 | 19 | ImageAlgo::save_image(out, filename_out); 20 | } 21 | } -------------------------------------------------------------------------------- /lib/vgncorr.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace vgncorr 6 | { 7 | struct Config 8 | { 9 | // if scale or blur are 0 both values are determined automatically 10 | int scale = 0; 11 | int blur = 0; 12 | int histogram_smoothing = 16; 13 | float delta_start_divider = 1.f; 14 | int delta_max_precision = 1024; 15 | }; 16 | 17 | void correct(Config const& config, std::string const& filename_in, std::string const& filename_out); 18 | 19 | } --------------------------------------------------------------------------------