├── BUILD ├── LICENSE ├── README.md ├── WORKSPACE ├── butteraugli ├── Makefile ├── butteraugli.cc ├── butteraugli.h └── butteraugli_main.cc ├── docs ├── comparison.html ├── vs_fuzz.svg ├── vs_fuzz_butteraugli_heatmap.png ├── vs_fuzz_left.png ├── vs_fuzz_right.png ├── vs_mae.svg ├── vs_mae_butteraugli_heatmap.png ├── vs_mae_left.png ├── vs_mae_right.png ├── vs_ncc.svg ├── vs_ncc_butteraugli_heatmap.png ├── vs_ncc_left.png ├── vs_ncc_right.png ├── vs_psnr.svg ├── vs_psnr_butteraugli_heatmap.png ├── vs_psnr_left.png ├── vs_psnr_right.png ├── vs_psnrhvs_m.svg ├── vs_psnrhvs_m_butteraugli_heatmap.png ├── vs_psnrhvs_m_left.png ├── vs_psnrhvs_m_right.png ├── vs_ssim.svg ├── vs_ssim_butteraugli_heatmap.png ├── vs_ssim_left.png ├── vs_ssim_right.png ├── vs_ssimulacra.svg ├── vs_ssimulacra_butteraugli_heatmap.png ├── vs_ssimulacra_left.png └── vs_ssimulacra_right.png ├── jpeg.BUILD ├── png.BUILD └── zlib.BUILD /BUILD: -------------------------------------------------------------------------------- 1 | cc_library( 2 | name = "butteraugli_lib", 3 | srcs = [ 4 | "butteraugli/butteraugli.cc", 5 | "butteraugli/butteraugli.h", 6 | ], 7 | hdrs = [ 8 | "butteraugli/butteraugli.h", 9 | ], 10 | copts = ["-Wno-sign-compare"], 11 | visibility = ["//visibility:public"], 12 | ) 13 | 14 | cc_binary( 15 | name = "butteraugli", 16 | srcs = ["butteraugli/butteraugli_main.cc"], 17 | copts = ["-Wno-sign-compare"], 18 | visibility = ["//visibility:public"], 19 | deps = [ 20 | ":butteraugli_lib", 21 | "@jpeg_archive//:jpeg", 22 | "@png_archive//:png", 23 | ], 24 | ) 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # butteraugli 2 | 3 | > A tool for measuring perceived differences between images 4 | 5 | ## Introduction 6 | 7 | Butteraugli is a project that estimates the psychovisual similarity of two 8 | images. It gives a score for the images that is reliable in the domain of barely 9 | noticeable differences. Butteraugli not only gives a scalar score, but also 10 | computes a spatial map of the level of differences. 11 | 12 | One of the main motivations for this project is the statistical differences in 13 | location and density of different color receptors, particularly the low density 14 | of blue cones in the fovea. Another motivation comes from more accurate modeling 15 | of ganglion cells, particularly the frequency space inhibition. 16 | 17 | ## Use 18 | 19 | Butteraugli can work as a quality metric for lossy image and video compression. 20 | On our small test corpus butteraugli performs better than our implementations of 21 | the reference methods, psnrhsv-m, ssim, and our yuv-color-space variant of ssim. 22 | One possible use is to define the quality level setting used in a jpeg 23 | compressor, or to compare two or more compression methods at the same level of 24 | psychovisual differences. 25 | 26 | Butteraugli is intended to be a research tool more than a practical tool for 27 | choosing compression formats. We don't know how well butteraugli performs with 28 | major deformations -- we have mostly tuned it within a small range of quality, 29 | roughly corresponding to jpeg qualities 90 to 95. 30 | 31 | ## Interface 32 | 33 | Only a C++ interface is provided. The interface takes two images and outputs a 34 | map together with a scalar value defining the difference. The scalar value can 35 | be compared to two reference values that divide the value space into three 36 | experience classes: 'great', 'acceptable' and 'not acceptable'. 37 | 38 | ## Build instructions 39 | 40 | Install [Bazel](http://bazel.build) by following the 41 | [instructions](https://www.bazel.build/docs/install.html). Run `bazel build -c opt 42 | //:butteraugli` in the directory that contains this README file to build the 43 | [command-line utility](#cmdline-tool). If you want to use Butteraugli as a 44 | library, depend on the `//:butteraugli_lib` target. 45 | 46 | Alternatively, you can use the Makefile provided in the `butteraugli` directory, 47 | after ensuring that [libpng](http://www.libpng.org/) and 48 | [libjpeg](http://ijg.org/) are installed. On some systems you might need to also 49 | install corresponding `-dev` packages. 50 | 51 | The code is portable and also compiles on Windows after defining 52 | `_CRT_SECURE_NO_WARNINGS` in the project settings. 53 | 54 | ## Command-line utility {#cmdline-tool} 55 | 56 | Butteraugli, apart from the library, comes bundled with a comparison tool. The 57 | comparison tool supports PNG and JPG images as inputs. To compare images, run: 58 | 59 | ``` 60 | butteraugli image1.{png|jpg} image2.{png|jpg} 61 | ``` 62 | 63 | The tool can also produce a heatmap of differences between images. The heatmap 64 | will be output as a PNM image. To produce one, run: 65 | 66 | ``` 67 | butteraugli image1.{png|jpg} image2.{png|jpg} heatmap.pnm 68 | ``` 69 | -------------------------------------------------------------------------------- /WORKSPACE: -------------------------------------------------------------------------------- 1 | workspace(name = "butteraugli") 2 | 3 | load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") 4 | 5 | http_archive( 6 | name = "png_archive", 7 | build_file = "@//:png.BUILD", 8 | sha256 = "e45ce5f68b1d80e2cb9a2b601605b374bdf51e1798ef1c2c2bd62131dfcf9eef", 9 | strip_prefix = "libpng-1.6.34", 10 | urls = [ 11 | "https://mirror.bazel.build/github.com/glennrp/libpng/archive/v1.6.34.tar.gz", 12 | "https://github.com/glennrp/libpng/archive/v1.6.34.tar.gz", 13 | ], 14 | ) 15 | 16 | http_archive( 17 | name = "zlib_archive", 18 | build_file = "@//:zlib.BUILD", 19 | sha256 = "c3e5e9fdd5004dcb542feda5ee4f0ff0744628baf8ed2dd5d66f8ca1197cb1a1", 20 | strip_prefix = "zlib-1.2.11", 21 | urls = [ 22 | "https://mirror.bazel.build/zlib.net/zlib-1.2.11.tar.gz", 23 | "https://zlib.net/zlib-1.2.11.tar.gz", 24 | ], 25 | ) 26 | 27 | http_archive( 28 | name = "jpeg_archive", 29 | build_file = "@//:jpeg.BUILD", 30 | sha256 = "240fd398da741669bf3c90366f58452ea59041cacc741a489b99f2f6a0bad052", 31 | strip_prefix = "jpeg-9b", 32 | urls = [ 33 | "https://mirror.bazel.build/www.ijg.org/files/jpegsrc.v9b.tar.gz", 34 | "http://www.ijg.org/files/jpegsrc.v9b.tar.gz", 35 | ], 36 | ) 37 | -------------------------------------------------------------------------------- /butteraugli/Makefile: -------------------------------------------------------------------------------- 1 | LDLIBS += -lpng -ljpeg 2 | CXXFLAGS += -std=c++11 -I.. 3 | LINK.o = $(LINK.cc) 4 | 5 | all: butteraugli.o butteraugli_main.o butteraugli 6 | 7 | butteraugli: butteraugli.o butteraugli_main.o 8 | -------------------------------------------------------------------------------- /butteraugli/butteraugli.cc: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Google Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | // Author: Jyrki Alakuijala (jyrki.alakuijala@gmail.com) 16 | // 17 | // The physical architecture of butteraugli is based on the following naming 18 | // convention: 19 | // * Opsin - dynamics of the photosensitive chemicals in the retina 20 | // with their immediate electrical processing 21 | // * Xyb - hybrid opponent/trichromatic color space 22 | // x is roughly red-subtract-green. 23 | // y is yellow. 24 | // b is blue. 25 | // Xyb values are computed from Opsin mixing, not directly from rgb. 26 | // * Mask - for visual masking 27 | // * Hf - color modeling for spatially high-frequency features 28 | // * Lf - color modeling for spatially low-frequency features 29 | // * Diffmap - to cluster and build an image of error between the images 30 | // * Blur - to hold the smoothing code 31 | 32 | #include "butteraugli/butteraugli.h" 33 | 34 | #include 35 | #include 36 | #include 37 | #include 38 | #include 39 | 40 | #include 41 | #include 42 | 43 | 44 | // Restricted pointers speed up Convolution(); MSVC uses a different keyword. 45 | #ifdef _MSC_VER 46 | #define __restrict__ __restrict 47 | #endif 48 | 49 | #ifndef PROFILER_ENABLED 50 | #define PROFILER_ENABLED 0 51 | #endif 52 | #if PROFILER_ENABLED 53 | #else 54 | #define PROFILER_FUNC 55 | #define PROFILER_ZONE(name) 56 | #endif 57 | 58 | namespace butteraugli { 59 | 60 | void *CacheAligned::Allocate(const size_t bytes) { 61 | PROFILER_FUNC; 62 | char *const allocated = static_cast(malloc(bytes + kCacheLineSize)); 63 | if (allocated == nullptr) { 64 | return nullptr; 65 | } 66 | const uintptr_t misalignment = 67 | reinterpret_cast(allocated) & (kCacheLineSize - 1); 68 | // malloc is at least kPointerSize aligned, so we can store the "allocated" 69 | // pointer immediately before the aligned memory. 70 | assert(misalignment % kPointerSize == 0); 71 | char *const aligned = allocated + kCacheLineSize - misalignment; 72 | memcpy(aligned - kPointerSize, &allocated, kPointerSize); 73 | return BUTTERAUGLI_ASSUME_ALIGNED(aligned, 64); 74 | } 75 | 76 | void CacheAligned::Free(void *aligned_pointer) { 77 | PROFILER_FUNC; 78 | if (aligned_pointer == nullptr) { 79 | return; 80 | } 81 | char *const aligned = static_cast(aligned_pointer); 82 | assert(reinterpret_cast(aligned) % kCacheLineSize == 0); 83 | char *allocated; 84 | memcpy(&allocated, aligned - kPointerSize, kPointerSize); 85 | assert(allocated <= aligned - kPointerSize); 86 | assert(allocated >= aligned - kCacheLineSize); 87 | free(allocated); 88 | } 89 | 90 | static inline bool IsNan(const float x) { 91 | uint32_t bits; 92 | memcpy(&bits, &x, sizeof(bits)); 93 | const uint32_t bitmask_exp = 0x7F800000; 94 | return (bits & bitmask_exp) == bitmask_exp && (bits & 0x7FFFFF); 95 | } 96 | 97 | static inline bool IsNan(const double x) { 98 | uint64_t bits; 99 | memcpy(&bits, &x, sizeof(bits)); 100 | return (0x7ff0000000000001ULL <= bits && bits <= 0x7fffffffffffffffULL) || 101 | (0xfff0000000000001ULL <= bits && bits <= 0xffffffffffffffffULL); 102 | } 103 | 104 | static inline void CheckImage(const ImageF &image, const char *name) { 105 | PROFILER_FUNC; 106 | for (size_t y = 0; y < image.ysize(); ++y) { 107 | const float * const BUTTERAUGLI_RESTRICT row = image.Row(y); 108 | for (size_t x = 0; x < image.xsize(); ++x) { 109 | if (IsNan(row[x])) { 110 | printf("Image %s @ %lu,%lu (of %lu,%lu)\n", name, x, y, image.xsize(), 111 | image.ysize()); 112 | exit(1); 113 | } 114 | } 115 | } 116 | } 117 | 118 | #if BUTTERAUGLI_ENABLE_CHECKS 119 | 120 | #define CHECK_NAN(x, str) \ 121 | do { \ 122 | if (IsNan(x)) { \ 123 | printf("%d: %s\n", __LINE__, str); \ 124 | abort(); \ 125 | } \ 126 | } while (0) 127 | 128 | #define CHECK_IMAGE(image, name) CheckImage(image, name) 129 | 130 | #else 131 | 132 | #define CHECK_NAN(x, str) 133 | #define CHECK_IMAGE(image, name) 134 | 135 | #endif 136 | 137 | 138 | // Purpose of kInternalGoodQualityThreshold: 139 | // Normalize 'ok' image degradation to 1.0 across different versions of 140 | // butteraugli. 141 | static const double kInternalGoodQualityThreshold = 20.35; 142 | static const double kGlobalScale = 1.0 / kInternalGoodQualityThreshold; 143 | 144 | inline float DotProduct(const float u[3], const float v[3]) { 145 | return u[0] * v[0] + u[1] * v[1] + u[2] * v[2]; 146 | } 147 | 148 | std::vector ComputeKernel(float sigma) { 149 | const float m = 2.25; // Accuracy increases when m is increased. 150 | const float scaler = -1.0 / (2 * sigma * sigma); 151 | const int diff = std::max(1, m * fabs(sigma)); 152 | std::vector kernel(2 * diff + 1); 153 | for (int i = -diff; i <= diff; ++i) { 154 | kernel[i + diff] = exp(scaler * i * i); 155 | } 156 | return kernel; 157 | } 158 | 159 | void ConvolveBorderColumn( 160 | const ImageF& in, 161 | const std::vector& kernel, 162 | const float weight_no_border, 163 | const float border_ratio, 164 | const size_t x, 165 | float* const BUTTERAUGLI_RESTRICT row_out) { 166 | const int offset = kernel.size() / 2; 167 | int minx = x < offset ? 0 : x - offset; 168 | int maxx = std::min(in.xsize() - 1, x + offset); 169 | float weight = 0.0f; 170 | for (int j = minx; j <= maxx; ++j) { 171 | weight += kernel[j - x + offset]; 172 | } 173 | // Interpolate linearly between the no-border scaling and border scaling. 174 | weight = (1.0f - border_ratio) * weight + border_ratio * weight_no_border; 175 | float scale = 1.0f / weight; 176 | for (size_t y = 0; y < in.ysize(); ++y) { 177 | const float* const BUTTERAUGLI_RESTRICT row_in = in.Row(y); 178 | float sum = 0.0f; 179 | for (int j = minx; j <= maxx; ++j) { 180 | sum += row_in[j] * kernel[j - x + offset]; 181 | } 182 | row_out[y] = sum * scale; 183 | } 184 | } 185 | 186 | // Computes a horizontal convolution and transposes the result. 187 | ImageF Convolution(const ImageF& in, 188 | const std::vector& kernel, 189 | const float border_ratio) { 190 | PROFILER_FUNC; 191 | ImageF out(in.ysize(), in.xsize()); 192 | const int len = kernel.size(); 193 | const int offset = kernel.size() / 2; 194 | float weight_no_border = 0.0f; 195 | for (int j = 0; j < len; ++j) { 196 | weight_no_border += kernel[j]; 197 | } 198 | float scale_no_border = 1.0f / weight_no_border; 199 | const int border1 = in.xsize() <= offset ? in.xsize() : offset; 200 | const int border2 = in.xsize() - offset; 201 | std::vector scaled_kernel = kernel; 202 | for (int i = 0; i < scaled_kernel.size(); ++i) { 203 | scaled_kernel[i] *= scale_no_border; 204 | } 205 | // left border 206 | for (int x = 0; x < border1; ++x) { 207 | ConvolveBorderColumn(in, kernel, weight_no_border, border_ratio, x, 208 | out.Row(x)); 209 | } 210 | // middle 211 | for (size_t y = 0; y < in.ysize(); ++y) { 212 | const float* const BUTTERAUGLI_RESTRICT row_in = in.Row(y); 213 | for (int x = border1; x < border2; ++x) { 214 | const int d = x - offset; 215 | float* const BUTTERAUGLI_RESTRICT row_out = out.Row(x); 216 | float sum = 0.0f; 217 | for (int j = 0; j < len; ++j) { 218 | sum += row_in[d + j] * scaled_kernel[j]; 219 | } 220 | row_out[y] = sum; 221 | } 222 | } 223 | // right border 224 | for (int x = border2; x < in.xsize(); ++x) { 225 | ConvolveBorderColumn(in, kernel, weight_no_border, border_ratio, x, 226 | out.Row(x)); 227 | } 228 | return out; 229 | } 230 | 231 | // A blur somewhat similar to a 2D Gaussian blur. 232 | // See: https://en.wikipedia.org/wiki/Gaussian_blur 233 | ImageF Blur(const ImageF& in, float sigma, float border_ratio) { 234 | std::vector kernel = ComputeKernel(sigma); 235 | return Convolution(Convolution(in, kernel, border_ratio), 236 | kernel, border_ratio); 237 | } 238 | 239 | // Clamping linear interpolator. 240 | inline double InterpolateClampNegative(const double *array, 241 | int size, double ix) { 242 | if (ix < 0) { 243 | ix = 0; 244 | } 245 | int baseix = static_cast(ix); 246 | double res; 247 | if (baseix >= size - 1) { 248 | res = array[size - 1]; 249 | } else { 250 | double mix = ix - baseix; 251 | int nextix = baseix + 1; 252 | res = array[baseix] + mix * (array[nextix] - array[baseix]); 253 | } 254 | return res; 255 | } 256 | 257 | double GammaMinArg() { 258 | double out0, out1, out2; 259 | OpsinAbsorbance(0.0, 0.0, 0.0, &out0, &out1, &out2); 260 | return std::min(out0, std::min(out1, out2)); 261 | } 262 | 263 | double GammaMaxArg() { 264 | double out0, out1, out2; 265 | OpsinAbsorbance(255.0, 255.0, 255.0, &out0, &out1, &out2); 266 | return std::max(out0, std::max(out1, out2)); 267 | } 268 | 269 | double SimpleGamma(double v) { 270 | static const double kGamma = 0.372322653176; 271 | static const double limit = 37.8000499603; 272 | double bright = v - limit; 273 | if (bright >= 0) { 274 | static const double mul = 0.0950819040934; 275 | v -= bright * mul; 276 | } 277 | { 278 | static const double limit2 = 74.6154406429; 279 | double bright2 = v - limit2; 280 | if (bright2 >= 0) { 281 | static const double mul = 0.01; 282 | v -= bright2 * mul; 283 | } 284 | } 285 | { 286 | static const double limit2 = 82.8505938033; 287 | double bright2 = v - limit2; 288 | if (bright2 >= 0) { 289 | static const double mul = 0.0316722592629; 290 | v -= bright2 * mul; 291 | } 292 | } 293 | { 294 | static const double limit2 = 92.8505938033; 295 | double bright2 = v - limit2; 296 | if (bright2 >= 0) { 297 | static const double mul = 0.221249885752; 298 | v -= bright2 * mul; 299 | } 300 | } 301 | { 302 | static const double limit2 = 102.8505938033; 303 | double bright2 = v - limit2; 304 | if (bright2 >= 0) { 305 | static const double mul = 0.0402547853939; 306 | v -= bright2 * mul; 307 | } 308 | } 309 | { 310 | static const double limit2 = 112.8505938033; 311 | double bright2 = v - limit2; 312 | if (bright2 >= 0) { 313 | static const double mul = 0.021471798711500003; 314 | v -= bright2 * mul; 315 | } 316 | } 317 | static const double offset = 0.106544447664; 318 | static const double scale = 10.7950943969; 319 | double retval = scale * (offset + pow(v, kGamma)); 320 | return retval; 321 | } 322 | 323 | static inline double Gamma(double v) { 324 | //return SimpleGamma(v); 325 | return GammaPolynomial(v); 326 | } 327 | 328 | std::vector OpsinDynamicsImage(const std::vector& rgb) { 329 | PROFILER_FUNC; 330 | std::vector xyb(3); 331 | std::vector blurred(3); 332 | const double kSigma = 1.2; 333 | for (int i = 0; i < 3; ++i) { 334 | xyb[i] = ImageF(rgb[i].xsize(), rgb[i].ysize()); 335 | blurred[i] = Blur(rgb[i], kSigma, 0.0); 336 | } 337 | for (size_t y = 0; y < rgb[0].ysize(); ++y) { 338 | const float* const BUTTERAUGLI_RESTRICT row_r = rgb[0].Row(y); 339 | const float* const BUTTERAUGLI_RESTRICT row_g = rgb[1].Row(y); 340 | const float* const BUTTERAUGLI_RESTRICT row_b = rgb[2].Row(y); 341 | const float* const BUTTERAUGLI_RESTRICT row_blurred_r = blurred[0].Row(y); 342 | const float* const BUTTERAUGLI_RESTRICT row_blurred_g = blurred[1].Row(y); 343 | const float* const BUTTERAUGLI_RESTRICT row_blurred_b = blurred[2].Row(y); 344 | float* const BUTTERAUGLI_RESTRICT row_out_x = xyb[0].Row(y); 345 | float* const BUTTERAUGLI_RESTRICT row_out_y = xyb[1].Row(y); 346 | float* const BUTTERAUGLI_RESTRICT row_out_b = xyb[2].Row(y); 347 | for (size_t x = 0; x < rgb[0].xsize(); ++x) { 348 | float sensitivity[3]; 349 | { 350 | // Calculate sensitivity based on the smoothed image gamma derivative. 351 | float pre_mixed0, pre_mixed1, pre_mixed2; 352 | OpsinAbsorbance(row_blurred_r[x], row_blurred_g[x], row_blurred_b[x], 353 | &pre_mixed0, &pre_mixed1, &pre_mixed2); 354 | // TODO: use new polynomial to compute Gamma(x)/x derivative. 355 | sensitivity[0] = Gamma(pre_mixed0) / pre_mixed0; 356 | sensitivity[1] = Gamma(pre_mixed1) / pre_mixed1; 357 | sensitivity[2] = Gamma(pre_mixed2) / pre_mixed2; 358 | } 359 | float cur_mixed0, cur_mixed1, cur_mixed2; 360 | OpsinAbsorbance(row_r[x], row_g[x], row_b[x], 361 | &cur_mixed0, &cur_mixed1, &cur_mixed2); 362 | cur_mixed0 *= sensitivity[0]; 363 | cur_mixed1 *= sensitivity[1]; 364 | cur_mixed2 *= sensitivity[2]; 365 | RgbToXyb(cur_mixed0, cur_mixed1, cur_mixed2, 366 | &row_out_x[x], &row_out_y[x], &row_out_b[x]); 367 | } 368 | } 369 | return xyb; 370 | } 371 | 372 | // Make area around zero less important (remove it). 373 | static BUTTERAUGLI_INLINE float RemoveRangeAroundZero(float w, float x) { 374 | return x > w ? x - w : x < -w ? x + w : 0.0f; 375 | } 376 | 377 | // Make area around zero more important (2x it until the limit). 378 | static BUTTERAUGLI_INLINE float AmplifyRangeAroundZero(float w, float x) { 379 | return x > w ? x + w : x < -w ? x - w : 2.0f * x; 380 | } 381 | 382 | // XybLowFreqToVals converts from low-frequency XYB space to the 'vals' space. 383 | // Vals space can be converted to L2-norm space (Euclidean and normalized) 384 | // through visual masking. 385 | template 386 | BUTTERAUGLI_INLINE void XybLowFreqToVals(const V &x, const V &y, const V &b_arg, 387 | V *BUTTERAUGLI_RESTRICT valx, 388 | V *BUTTERAUGLI_RESTRICT valy, 389 | V *BUTTERAUGLI_RESTRICT valb) { 390 | static const double xmuli = 5.57547552483; 391 | static const double ymuli = 1.20828034498; 392 | static const double bmuli = 6.08319517575; 393 | static const double y_to_b_muli = -0.628811683685; 394 | 395 | const V xmul(xmuli); 396 | const V ymul(ymuli); 397 | const V bmul(bmuli); 398 | const V y_to_b_mul(y_to_b_muli); 399 | const V b = b_arg + y_to_b_mul * y; 400 | *valb = b * bmul; 401 | *valx = x * xmul; 402 | *valy = y * ymul; 403 | } 404 | 405 | static ImageF SuppressInBrightAreas(size_t xsize, size_t ysize, 406 | double mul, double mul2, double reg, 407 | const ImageF& hf, 408 | const ImageF& brightness) { 409 | PROFILER_FUNC; 410 | ImageF inew(xsize, ysize); 411 | for (size_t y = 0; y < ysize; ++y) { 412 | const float* const rowhf = hf.Row(y); 413 | const float* const rowbr = brightness.Row(y); 414 | float* const rownew = inew.Row(y); 415 | for (size_t x = 0; x < xsize; ++x) { 416 | float v = rowhf[x]; 417 | float scaler = mul * reg / (reg + rowbr[x]); 418 | rownew[x] = scaler * v; 419 | } 420 | } 421 | return inew; 422 | } 423 | 424 | 425 | static float SuppressHfInBrightAreas(float hf, float brightness, 426 | float mul, float reg) { 427 | float scaler = mul * reg / (reg + brightness); 428 | return scaler * hf; 429 | } 430 | 431 | static float SuppressUhfInBrightAreas(float hf, float brightness, 432 | float mul, float reg) { 433 | float scaler = mul * reg / (reg + brightness); 434 | return scaler * hf; 435 | } 436 | 437 | static float MaximumClamp(float v, float maxval) { 438 | static const double kMul = 0.688059627878; 439 | if (v >= maxval) { 440 | v -= maxval; 441 | v *= kMul; 442 | v += maxval; 443 | } else if (v < -maxval) { 444 | v += maxval; 445 | v *= kMul; 446 | v -= maxval; 447 | } 448 | return v; 449 | } 450 | 451 | static ImageF MaximumClamping(size_t xsize, size_t ysize, const ImageF& ix, 452 | double yw) { 453 | static const double kMul = 0.688059627878; 454 | ImageF inew(xsize, ysize); 455 | for (size_t y = 0; y < ysize; ++y) { 456 | const float* const rowx = ix.Row(y); 457 | float* const rownew = inew.Row(y); 458 | for (size_t x = 0; x < xsize; ++x) { 459 | double v = rowx[x]; 460 | if (v >= yw) { 461 | v -= yw; 462 | v *= kMul; 463 | v += yw; 464 | } else if (v < -yw) { 465 | v += yw; 466 | v *= kMul; 467 | v -= yw; 468 | } 469 | rownew[x] = v; 470 | } 471 | } 472 | return inew; 473 | } 474 | 475 | static ImageF SuppressXByY(size_t xsize, size_t ysize, 476 | const ImageF& ix, const ImageF& iy, 477 | const double yw) { 478 | static const double s = 0.745954517135; 479 | ImageF inew(xsize, ysize); 480 | for (size_t y = 0; y < ysize; ++y) { 481 | const float* const rowx = ix.Row(y); 482 | const float* const rowy = iy.Row(y); 483 | float* const rownew = inew.Row(y); 484 | for (size_t x = 0; x < xsize; ++x) { 485 | const double xval = rowx[x]; 486 | const double yval = rowy[x]; 487 | const double scaler = s + (yw * (1.0 - s)) / (yw + yval * yval); 488 | rownew[x] = scaler * xval; 489 | } 490 | } 491 | return inew; 492 | } 493 | 494 | static void SeparateFrequencies( 495 | size_t xsize, size_t ysize, 496 | const std::vector& xyb, 497 | PsychoImage &ps) { 498 | PROFILER_FUNC; 499 | ps.lf.resize(3); // XYB 500 | ps.mf.resize(3); // XYB 501 | ps.hf.resize(2); // XY 502 | ps.uhf.resize(2); // XY 503 | // Extract lf ... 504 | static const double kSigmaLf = 7.46953768697; 505 | static const double kSigmaHf = 3.734768843485; 506 | static const double kSigmaUhf = 1.8673844217425; 507 | // At borders we move some more of the energy to the high frequency 508 | // parts, because there can be unfortunate continuations in tiling 509 | // background color etc. So we want to represent the borders with 510 | // some more accuracy. 511 | static double border_lf = -0.00457628248637; 512 | static double border_mf = -0.271277366628; 513 | static double border_hf = 0.147068973249; 514 | for (int i = 0; i < 3; ++i) { 515 | ps.lf[i] = Blur(xyb[i], kSigmaLf, border_lf); 516 | // ... and keep everything else in mf. 517 | ps.mf[i] = ImageF(xsize, ysize); 518 | for (size_t y = 0; y < ysize; ++y) { 519 | for (size_t x = 0; x < xsize; ++x) { 520 | ps.mf[i].Row(y)[x] = xyb[i].Row(y)[x] - ps.lf[i].Row(y)[x]; 521 | } 522 | } 523 | if (i == 2) { 524 | ps.mf[i] = Blur(ps.mf[i], kSigmaHf, border_mf); 525 | break; 526 | } 527 | // Divide mf into mf and hf. 528 | ps.hf[i] = ImageF(xsize, ysize); 529 | for (size_t y = 0; y < ysize; ++y) { 530 | float* BUTTERAUGLI_RESTRICT const row_mf = ps.mf[i].Row(y); 531 | float* BUTTERAUGLI_RESTRICT const row_hf = ps.hf[i].Row(y); 532 | for (size_t x = 0; x < xsize; ++x) { 533 | row_hf[x] = row_mf[x]; 534 | } 535 | } 536 | ps.mf[i] = Blur(ps.mf[i], kSigmaHf, border_mf); 537 | static const double w0 = 0.120079806822; 538 | static const double w1 = 0.03430529365; 539 | if (i == 0) { 540 | for (size_t y = 0; y < ysize; ++y) { 541 | float* BUTTERAUGLI_RESTRICT const row_mf = ps.mf[0].Row(y); 542 | float* BUTTERAUGLI_RESTRICT const row_hf = ps.hf[0].Row(y); 543 | for (size_t x = 0; x < xsize; ++x) { 544 | row_hf[x] -= row_mf[x]; 545 | row_mf[x] = RemoveRangeAroundZero(w0, row_mf[x]); 546 | } 547 | } 548 | } else { 549 | for (size_t y = 0; y < ysize; ++y) { 550 | float* BUTTERAUGLI_RESTRICT const row_mf = ps.mf[1].Row(y); 551 | float* BUTTERAUGLI_RESTRICT const row_hf = ps.hf[1].Row(y); 552 | for (size_t x = 0; x < xsize; ++x) { 553 | row_hf[x] -= row_mf[x]; 554 | row_mf[x] = AmplifyRangeAroundZero(w1, row_mf[x]); 555 | } 556 | } 557 | } 558 | } 559 | // Suppress red-green by intensity change in the high freq channels. 560 | static const double suppress = 2.96534974403; 561 | ps.hf[0] = SuppressXByY(xsize, ysize, ps.hf[0], ps.hf[1], suppress); 562 | 563 | for (int i = 0; i < 2; ++i) { 564 | // Divide hf into hf and uhf. 565 | ps.uhf[i] = ImageF(xsize, ysize); 566 | for (size_t y = 0; y < ysize; ++y) { 567 | float* BUTTERAUGLI_RESTRICT const row_uhf = ps.uhf[i].Row(y); 568 | float* BUTTERAUGLI_RESTRICT const row_hf = ps.hf[i].Row(y); 569 | for (size_t x = 0; x < xsize; ++x) { 570 | row_uhf[x] = row_hf[x]; 571 | } 572 | } 573 | ps.hf[i] = Blur(ps.hf[i], kSigmaUhf, border_hf); 574 | static const double kRemoveHfRange = 0.0287615200377; 575 | static const double kMaxclampHf = 78.8223237675; 576 | static const double kMaxclampUhf = 5.8907152736; 577 | static const float kMulSuppressHf = 1.10684769012; 578 | static const float kMulRegHf = 0.478741530298; 579 | static const float kRegHf = 2000 * kMulRegHf; 580 | static const float kMulSuppressUhf = 1.76905001176; 581 | static const float kMulRegUhf = 0.310148420674; 582 | static const float kRegUhf = 2000 * kMulRegUhf; 583 | 584 | if (i == 0) { 585 | for (size_t y = 0; y < ysize; ++y) { 586 | float* BUTTERAUGLI_RESTRICT const row_uhf = ps.uhf[0].Row(y); 587 | float* BUTTERAUGLI_RESTRICT const row_hf = ps.hf[0].Row(y); 588 | for (size_t x = 0; x < xsize; ++x) { 589 | row_uhf[x] -= row_hf[x]; 590 | row_hf[x] = RemoveRangeAroundZero(kRemoveHfRange, row_hf[x]); 591 | } 592 | } 593 | } else { 594 | for (size_t y = 0; y < ysize; ++y) { 595 | float* BUTTERAUGLI_RESTRICT const row_uhf = ps.uhf[1].Row(y); 596 | float* BUTTERAUGLI_RESTRICT const row_hf = ps.hf[1].Row(y); 597 | float* BUTTERAUGLI_RESTRICT const row_lf = ps.lf[1].Row(y); 598 | for (size_t x = 0; x < xsize; ++x) { 599 | row_uhf[x] -= row_hf[x]; 600 | row_hf[x] = MaximumClamp(row_hf[x], kMaxclampHf); 601 | row_uhf[x] = MaximumClamp(row_uhf[x], kMaxclampUhf); 602 | row_uhf[x] = SuppressUhfInBrightAreas(row_uhf[x], row_lf[x], 603 | kMulSuppressUhf, kRegUhf); 604 | row_hf[x] = SuppressHfInBrightAreas(row_hf[x], row_lf[x], 605 | kMulSuppressHf, kRegHf); 606 | 607 | } 608 | } 609 | } 610 | } 611 | // Modify range around zero code only concerns the high frequency 612 | // planes and only the X and Y channels. 613 | // Convert low freq xyb to vals space so that we can do a simple squared sum 614 | // diff on the low frequencies later. 615 | for (size_t y = 0; y < ysize; ++y) { 616 | float* BUTTERAUGLI_RESTRICT const row_x = ps.lf[0].Row(y); 617 | float* BUTTERAUGLI_RESTRICT const row_y = ps.lf[1].Row(y); 618 | float* BUTTERAUGLI_RESTRICT const row_b = ps.lf[2].Row(y); 619 | for (size_t x = 0; x < xsize; ++x) { 620 | float valx, valy, valb; 621 | XybLowFreqToVals(row_x[x], row_y[x], row_b[x], &valx, &valy, &valb); 622 | row_x[x] = valx; 623 | row_y[x] = valy; 624 | row_b[x] = valb; 625 | } 626 | } 627 | } 628 | 629 | static void SameNoiseLevels(const ImageF& i0, const ImageF& i1, 630 | const double kSigma, 631 | const double w, 632 | const double maxclamp, 633 | ImageF* BUTTERAUGLI_RESTRICT diffmap) { 634 | ImageF blurred(i0.xsize(), i0.ysize()); 635 | for (size_t y = 0; y < i0.ysize(); ++y) { 636 | const float* BUTTERAUGLI_RESTRICT const row0 = i0.Row(y); 637 | const float* BUTTERAUGLI_RESTRICT const row1 = i1.Row(y); 638 | float* BUTTERAUGLI_RESTRICT const to = blurred.Row(y); 639 | for (size_t x = 0; x < i0.xsize(); ++x) { 640 | double v0 = fabs(row0[x]); 641 | double v1 = fabs(row1[x]); 642 | if (v0 > maxclamp) v0 = maxclamp; 643 | if (v1 > maxclamp) v1 = maxclamp; 644 | to[x] = v0 - v1; 645 | } 646 | 647 | } 648 | blurred = Blur(blurred, kSigma, 0.0); 649 | for (size_t y = 0; y < i0.ysize(); ++y) { 650 | const float* BUTTERAUGLI_RESTRICT const row = blurred.Row(y); 651 | float* BUTTERAUGLI_RESTRICT const row_diff = diffmap->Row(y); 652 | for (size_t x = 0; x < i0.xsize(); ++x) { 653 | double diff = row[x]; 654 | row_diff[x] += w * diff * diff; 655 | } 656 | } 657 | } 658 | 659 | static void L2Diff(const ImageF& i0, const ImageF& i1, const double w, 660 | ImageF* BUTTERAUGLI_RESTRICT diffmap) { 661 | if (w == 0) { 662 | return; 663 | } 664 | for (size_t y = 0; y < i0.ysize(); ++y) { 665 | const float* BUTTERAUGLI_RESTRICT const row0 = i0.Row(y); 666 | const float* BUTTERAUGLI_RESTRICT const row1 = i1.Row(y); 667 | float* BUTTERAUGLI_RESTRICT const row_diff = diffmap->Row(y); 668 | for (size_t x = 0; x < i0.xsize(); ++x) { 669 | double diff = row0[x] - row1[x]; 670 | row_diff[x] += w * diff * diff; 671 | } 672 | } 673 | } 674 | 675 | // i0 is the original image. 676 | // i1 is the deformed copy. 677 | static void L2DiffAsymmetric(const ImageF& i0, const ImageF& i1, 678 | double w_0gt1, 679 | double w_0lt1, 680 | ImageF* BUTTERAUGLI_RESTRICT diffmap) { 681 | if (w_0gt1 == 0 && w_0lt1 == 0) { 682 | return; 683 | } 684 | w_0gt1 *= 0.8; 685 | w_0lt1 *= 0.8; 686 | for (size_t y = 0; y < i0.ysize(); ++y) { 687 | const float* BUTTERAUGLI_RESTRICT const row0 = i0.Row(y); 688 | const float* BUTTERAUGLI_RESTRICT const row1 = i1.Row(y); 689 | float* BUTTERAUGLI_RESTRICT const row_diff = diffmap->Row(y); 690 | for (size_t x = 0; x < i0.xsize(); ++x) { 691 | // Primary symmetric quadratic objective. 692 | double diff = row0[x] - row1[x]; 693 | row_diff[x] += w_0gt1 * diff * diff; 694 | 695 | // Secondary half-open quadratic objectives. 696 | const double fabs0 = fabs(row0[x]); 697 | const double too_small = 0.4 * fabs0; 698 | const double too_big = 1.0 * fabs0; 699 | 700 | if (row0[x] < 0) { 701 | if (row1[x] > -too_small) { 702 | double v = row1[x] + too_small; 703 | row_diff[x] += w_0lt1 * v * v; 704 | } else if (row1[x] < -too_big) { 705 | double v = -row1[x] - too_big; 706 | row_diff[x] += w_0lt1 * v * v; 707 | } 708 | } else { 709 | if (row1[x] < too_small) { 710 | double v = too_small - row1[x]; 711 | row_diff[x] += w_0lt1 * v * v; 712 | } else if (row1[x] > too_big) { 713 | double v = row1[x] - too_big; 714 | row_diff[x] += w_0lt1 * v * v; 715 | } 716 | } 717 | } 718 | } 719 | } 720 | 721 | // Making a cluster of local errors to be more impactful than 722 | // just a single error. 723 | ImageF CalculateDiffmap(const ImageF& diffmap_in) { 724 | PROFILER_FUNC; 725 | // Take square root. 726 | ImageF diffmap(diffmap_in.xsize(), diffmap_in.ysize()); 727 | static const float kInitialSlope = 100.0f; 728 | for (size_t y = 0; y < diffmap.ysize(); ++y) { 729 | const float* const BUTTERAUGLI_RESTRICT row_in = diffmap_in.Row(y); 730 | float* const BUTTERAUGLI_RESTRICT row_out = diffmap.Row(y); 731 | for (size_t x = 0; x < diffmap.xsize(); ++x) { 732 | const float orig_val = row_in[x]; 733 | // TODO(b/29974893): Until that is fixed do not call sqrt on very small 734 | // numbers. 735 | row_out[x] = (orig_val < (1.0f / (kInitialSlope * kInitialSlope)) 736 | ? kInitialSlope * orig_val 737 | : std::sqrt(orig_val)); 738 | } 739 | } 740 | return diffmap; 741 | } 742 | 743 | void MaskPsychoImage(const PsychoImage& pi0, const PsychoImage& pi1, 744 | const size_t xsize, const size_t ysize, 745 | std::vector* BUTTERAUGLI_RESTRICT mask, 746 | std::vector* BUTTERAUGLI_RESTRICT mask_dc) { 747 | std::vector mask_xyb0 = CreatePlanes(xsize, ysize, 3); 748 | std::vector mask_xyb1 = CreatePlanes(xsize, ysize, 3); 749 | static const double muls[4] = { 750 | 0, 751 | 1.64178305129, 752 | 0.831081703362, 753 | 3.23680933546, 754 | }; 755 | for (int i = 0; i < 2; ++i) { 756 | double a = muls[2 * i]; 757 | double b = muls[2 * i + 1]; 758 | for (size_t y = 0; y < ysize; ++y) { 759 | const float* const BUTTERAUGLI_RESTRICT row_hf0 = pi0.hf[i].Row(y); 760 | const float* const BUTTERAUGLI_RESTRICT row_hf1 = pi1.hf[i].Row(y); 761 | const float* const BUTTERAUGLI_RESTRICT row_uhf0 = pi0.uhf[i].Row(y); 762 | const float* const BUTTERAUGLI_RESTRICT row_uhf1 = pi1.uhf[i].Row(y); 763 | float* const BUTTERAUGLI_RESTRICT row0 = mask_xyb0[i].Row(y); 764 | float* const BUTTERAUGLI_RESTRICT row1 = mask_xyb1[i].Row(y); 765 | for (size_t x = 0; x < xsize; ++x) { 766 | row0[x] = a * row_uhf0[x] + b * row_hf0[x]; 767 | row1[x] = a * row_uhf1[x] + b * row_hf1[x]; 768 | } 769 | } 770 | } 771 | Mask(mask_xyb0, mask_xyb1, mask, mask_dc); 772 | } 773 | 774 | ButteraugliComparator::ButteraugliComparator(const std::vector& rgb0, 775 | double hf_asymmetry) 776 | : xsize_(rgb0[0].xsize()), 777 | ysize_(rgb0[0].ysize()), 778 | num_pixels_(xsize_ * ysize_), 779 | hf_asymmetry_(hf_asymmetry) { 780 | if (xsize_ < 8 || ysize_ < 8) return; 781 | std::vector xyb0 = OpsinDynamicsImage(rgb0); 782 | SeparateFrequencies(xsize_, ysize_, xyb0, pi0_); 783 | } 784 | 785 | void ButteraugliComparator::Mask( 786 | std::vector* BUTTERAUGLI_RESTRICT mask, 787 | std::vector* BUTTERAUGLI_RESTRICT mask_dc) const { 788 | MaskPsychoImage(pi0_, pi0_, xsize_, ysize_, mask, mask_dc); 789 | } 790 | 791 | void ButteraugliComparator::Diffmap(const std::vector& rgb1, 792 | ImageF &result) const { 793 | PROFILER_FUNC; 794 | if (xsize_ < 8 || ysize_ < 8) return; 795 | DiffmapOpsinDynamicsImage(OpsinDynamicsImage(rgb1), result); 796 | } 797 | 798 | void ButteraugliComparator::DiffmapOpsinDynamicsImage( 799 | const std::vector& xyb1, 800 | ImageF &result) const { 801 | PROFILER_FUNC; 802 | if (xsize_ < 8 || ysize_ < 8) return; 803 | PsychoImage pi1; 804 | SeparateFrequencies(xsize_, ysize_, xyb1, pi1); 805 | result = ImageF(xsize_, ysize_); 806 | DiffmapPsychoImage(pi1, result); 807 | } 808 | 809 | void ButteraugliComparator::DiffmapPsychoImage(const PsychoImage& pi1, 810 | ImageF& result) const { 811 | PROFILER_FUNC; 812 | if (xsize_ < 8 || ysize_ < 8) { 813 | return; 814 | } 815 | std::vector block_diff_dc(3); 816 | std::vector block_diff_ac(3); 817 | for (int c = 0; c < 3; ++c) { 818 | block_diff_dc[c] = ImageF(xsize_, ysize_, 0.0); 819 | block_diff_ac[c] = ImageF(xsize_, ysize_, 0.0); 820 | } 821 | 822 | static const double wUhfMalta = 5.1409625726; 823 | static const double norm1Uhf = 58.5001247061; 824 | MaltaDiffMap(pi0_.uhf[1], pi1.uhf[1], 825 | wUhfMalta * hf_asymmetry_, 826 | wUhfMalta / hf_asymmetry_, 827 | norm1Uhf, 828 | &block_diff_ac[1]); 829 | 830 | static const double wUhfMaltaX = 4.91743441556; 831 | static const double norm1UhfX = 687196.39002; 832 | MaltaDiffMap(pi0_.uhf[0], pi1.uhf[0], 833 | wUhfMaltaX * hf_asymmetry_, 834 | wUhfMaltaX / hf_asymmetry_, 835 | norm1UhfX, 836 | &block_diff_ac[0]); 837 | 838 | static const double wHfMalta = 153.671655716; 839 | static const double norm1Hf = 83150785.9592; 840 | MaltaDiffMapLF(pi0_.hf[1], pi1.hf[1], 841 | wHfMalta * sqrt(hf_asymmetry_), 842 | wHfMalta / sqrt(hf_asymmetry_), 843 | norm1Hf, 844 | &block_diff_ac[1]); 845 | 846 | static const double wHfMaltaX = 668.358918152; 847 | static const double norm1HfX = 0.882954368025; 848 | MaltaDiffMapLF(pi0_.hf[0], pi1.hf[0], 849 | wHfMaltaX * sqrt(hf_asymmetry_), 850 | wHfMaltaX / sqrt(hf_asymmetry_), 851 | norm1HfX, 852 | &block_diff_ac[0]); 853 | 854 | static const double wMfMalta = 6841.81248144; 855 | static const double norm1Mf = 0.0135134962487; 856 | MaltaDiffMapLF(pi0_.mf[1], pi1.mf[1], wMfMalta, wMfMalta, norm1Mf, 857 | &block_diff_ac[1]); 858 | 859 | static const double wMfMaltaX = 813.901703816; 860 | static const double norm1MfX = 16792.9322251; 861 | MaltaDiffMapLF(pi0_.mf[0], pi1.mf[0], wMfMaltaX, wMfMaltaX, norm1MfX, 862 | &block_diff_ac[0]); 863 | 864 | static const double wmul[9] = { 865 | 0, 866 | 32.4449876135, 867 | 0, 868 | 0, 869 | 0, 870 | 0, 871 | 1.01370836411, 872 | 0, 873 | 1.74566011615, 874 | }; 875 | 876 | static const double maxclamp = 85.7047444518; 877 | static const double kSigmaHfX = 10.6666499623; 878 | static const double w = 884.809801415; 879 | SameNoiseLevels(pi0_.hf[1], pi1.hf[1], kSigmaHfX, w, maxclamp, 880 | &block_diff_ac[1]); 881 | 882 | for (int c = 0; c < 3; ++c) { 883 | if (c < 2) { 884 | L2DiffAsymmetric(pi0_.hf[c], pi1.hf[c], 885 | wmul[c] * hf_asymmetry_, 886 | wmul[c] / hf_asymmetry_, 887 | &block_diff_ac[c]); 888 | } 889 | L2Diff(pi0_.mf[c], pi1.mf[c], wmul[3 + c], &block_diff_ac[c]); 890 | L2Diff(pi0_.lf[c], pi1.lf[c], wmul[6 + c], &block_diff_dc[c]); 891 | } 892 | 893 | std::vector mask_xyb; 894 | std::vector mask_xyb_dc; 895 | MaskPsychoImage(pi0_, pi1, xsize_, ysize_, &mask_xyb, &mask_xyb_dc); 896 | 897 | result = CalculateDiffmap( 898 | CombineChannels(mask_xyb, mask_xyb_dc, block_diff_dc, block_diff_ac)); 899 | } 900 | 901 | // Allows PaddedMaltaUnit to call either function via overloading. 902 | struct MaltaTagLF {}; 903 | struct MaltaTag {}; 904 | 905 | static float MaltaUnit(MaltaTagLF, const float* BUTTERAUGLI_RESTRICT d, 906 | const int xs) { 907 | const int xs3 = 3 * xs; 908 | float retval = 0; 909 | { 910 | // x grows, y constant 911 | float sum = 912 | d[-4] + 913 | d[-2] + 914 | d[0] + 915 | d[2] + 916 | d[4]; 917 | retval += sum * sum; 918 | } 919 | { 920 | // y grows, x constant 921 | float sum = 922 | d[-xs3 - xs] + 923 | d[-xs - xs] + 924 | d[0] + 925 | d[xs + xs] + 926 | d[xs3 + xs]; 927 | retval += sum * sum; 928 | } 929 | { 930 | // both grow 931 | float sum = 932 | d[-xs3 - 3] + 933 | d[-xs - xs - 2] + 934 | d[0] + 935 | d[xs + xs + 2] + 936 | d[xs3 + 3]; 937 | retval += sum * sum; 938 | } 939 | { 940 | // y grows, x shrinks 941 | float sum = 942 | d[-xs3 + 3] + 943 | d[-xs - xs + 2] + 944 | d[0] + 945 | d[xs + xs - 2] + 946 | d[xs3 - 3]; 947 | retval += sum * sum; 948 | } 949 | { 950 | // y grows -4 to 4, x shrinks 1 -> -1 951 | float sum = 952 | d[-xs3 - xs + 1] + 953 | d[-xs - xs + 1] + 954 | d[0] + 955 | d[xs + xs - 1] + 956 | d[xs3 + xs - 1]; 957 | retval += sum * sum; 958 | } 959 | { 960 | // y grows -4 to 4, x grows -1 -> 1 961 | float sum = 962 | d[-xs3 - xs - 1] + 963 | d[-xs - xs - 1] + 964 | d[0] + 965 | d[xs + xs + 1] + 966 | d[xs3 + xs + 1]; 967 | retval += sum * sum; 968 | } 969 | { 970 | // x grows -4 to 4, y grows -1 to 1 971 | float sum = 972 | d[-4 - xs] + 973 | d[-2 - xs] + 974 | d[0] + 975 | d[2 + xs] + 976 | d[4 + xs]; 977 | retval += sum * sum; 978 | } 979 | { 980 | // x grows -4 to 4, y shrinks 1 to -1 981 | float sum = 982 | d[-4 + xs] + 983 | d[-2 + xs] + 984 | d[0] + 985 | d[2 - xs] + 986 | d[4 - xs]; 987 | retval += sum * sum; 988 | } 989 | { 990 | /* 0_________ 991 | 1__*______ 992 | 2___*_____ 993 | 3_________ 994 | 4____0____ 995 | 5_________ 996 | 6_____*___ 997 | 7______*__ 998 | 8_________ */ 999 | float sum = 1000 | d[-xs3 - 2] + 1001 | d[-xs - xs - 1] + 1002 | d[0] + 1003 | d[xs + xs + 1] + 1004 | d[xs3 + 2]; 1005 | retval += sum * sum; 1006 | } 1007 | { 1008 | /* 0_________ 1009 | 1______*__ 1010 | 2_____*___ 1011 | 3_________ 1012 | 4____0____ 1013 | 5_________ 1014 | 6___*_____ 1015 | 7__*______ 1016 | 8_________ */ 1017 | float sum = 1018 | d[-xs3 + 2] + 1019 | d[-xs - xs + 1] + 1020 | d[0] + 1021 | d[xs + xs - 1] + 1022 | d[xs3 - 2]; 1023 | retval += sum * sum; 1024 | } 1025 | { 1026 | /* 0_________ 1027 | 1_________ 1028 | 2_*_______ 1029 | 3__*______ 1030 | 4____0____ 1031 | 5______*__ 1032 | 6_______*_ 1033 | 7_________ 1034 | 8_________ */ 1035 | float sum = 1036 | d[-xs - xs - 3] + 1037 | d[-xs - 2] + 1038 | d[0] + 1039 | d[xs + 2] + 1040 | d[xs + xs + 3]; 1041 | retval += sum * sum; 1042 | } 1043 | { 1044 | /* 0_________ 1045 | 1_________ 1046 | 2_______*_ 1047 | 3______*__ 1048 | 4____0____ 1049 | 5__*______ 1050 | 6_*_______ 1051 | 7_________ 1052 | 8_________ */ 1053 | float sum = 1054 | d[-xs - xs + 3] + 1055 | d[-xs + 2] + 1056 | d[0] + 1057 | d[xs - 2] + 1058 | d[xs + xs - 3]; 1059 | retval += sum * sum; 1060 | } 1061 | { 1062 | /* 0_________ 1063 | 1_________ 1064 | 2________* 1065 | 3______*__ 1066 | 4____0____ 1067 | 5__*______ 1068 | 6*________ 1069 | 7_________ 1070 | 8_________ */ 1071 | 1072 | float sum = 1073 | d[xs + xs - 4] + 1074 | d[xs - 2] + 1075 | d[0] + 1076 | d[-xs + 2] + 1077 | d[-xs - xs + 4]; 1078 | retval += sum * sum; 1079 | } 1080 | { 1081 | /* 0_________ 1082 | 1_________ 1083 | 2*________ 1084 | 3__*______ 1085 | 4____0____ 1086 | 5______*__ 1087 | 6________* 1088 | 7_________ 1089 | 8_________ */ 1090 | float sum = 1091 | d[-xs - xs - 4] + 1092 | d[-xs - 2] + 1093 | d[0] + 1094 | d[xs + 2] + 1095 | d[xs + xs + 4]; 1096 | retval += sum * sum; 1097 | } 1098 | { 1099 | /* 0__*______ 1100 | 1_________ 1101 | 2___*_____ 1102 | 3_________ 1103 | 4____0____ 1104 | 5_________ 1105 | 6_____*___ 1106 | 7_________ 1107 | 8______*__ */ 1108 | float sum = 1109 | d[-xs3 - xs - 2] + 1110 | d[-xs - xs - 1] + 1111 | d[0] + 1112 | d[xs + xs + 1] + 1113 | d[xs3 + xs + 2]; 1114 | retval += sum * sum; 1115 | } 1116 | { 1117 | /* 0______*__ 1118 | 1_________ 1119 | 2_____*___ 1120 | 3_________ 1121 | 4____0____ 1122 | 5_________ 1123 | 6___*_____ 1124 | 7_________ 1125 | 8__*______ */ 1126 | float sum = 1127 | d[-xs3 - xs + 2] + 1128 | d[-xs - xs + 1] + 1129 | d[0] + 1130 | d[xs + xs - 1] + 1131 | d[xs3 + xs - 2]; 1132 | retval += sum * sum; 1133 | } 1134 | return retval; 1135 | } 1136 | 1137 | static float MaltaUnit(MaltaTag, const float* BUTTERAUGLI_RESTRICT d, 1138 | const int xs) { 1139 | const int xs3 = 3 * xs; 1140 | float retval = 0; 1141 | { 1142 | // x grows, y constant 1143 | float sum = 1144 | d[-4] + 1145 | d[-3] + 1146 | d[-2] + 1147 | d[-1] + 1148 | d[0] + 1149 | d[1] + 1150 | d[2] + 1151 | d[3] + 1152 | d[4]; 1153 | retval += sum * sum; 1154 | } 1155 | { 1156 | // y grows, x constant 1157 | float sum = 1158 | d[-xs3 - xs] + 1159 | d[-xs3] + 1160 | d[-xs - xs] + 1161 | d[-xs] + 1162 | d[0] + 1163 | d[xs] + 1164 | d[xs + xs] + 1165 | d[xs3] + 1166 | d[xs3 + xs]; 1167 | retval += sum * sum; 1168 | } 1169 | { 1170 | // both grow 1171 | float sum = 1172 | d[-xs3 - 3] + 1173 | d[-xs - xs - 2] + 1174 | d[-xs - 1] + 1175 | d[0] + 1176 | d[xs + 1] + 1177 | d[xs + xs + 2] + 1178 | d[xs3 + 3]; 1179 | retval += sum * sum; 1180 | } 1181 | { 1182 | // y grows, x shrinks 1183 | float sum = 1184 | d[-xs3 + 3] + 1185 | d[-xs - xs + 2] + 1186 | d[-xs + 1] + 1187 | d[0] + 1188 | d[xs - 1] + 1189 | d[xs + xs - 2] + 1190 | d[xs3 - 3]; 1191 | retval += sum * sum; 1192 | } 1193 | { 1194 | // y grows -4 to 4, x shrinks 1 -> -1 1195 | float sum = 1196 | d[-xs3 - xs + 1] + 1197 | d[-xs3 + 1] + 1198 | d[-xs - xs + 1] + 1199 | d[-xs] + 1200 | d[0] + 1201 | d[xs] + 1202 | d[xs + xs - 1] + 1203 | d[xs3 - 1] + 1204 | d[xs3 + xs - 1]; 1205 | retval += sum * sum; 1206 | } 1207 | { 1208 | // y grows -4 to 4, x grows -1 -> 1 1209 | float sum = 1210 | d[-xs3 - xs - 1] + 1211 | d[-xs3 - 1] + 1212 | d[-xs - xs - 1] + 1213 | d[-xs] + 1214 | d[0] + 1215 | d[xs] + 1216 | d[xs + xs + 1] + 1217 | d[xs3 + 1] + 1218 | d[xs3 + xs + 1]; 1219 | retval += sum * sum; 1220 | } 1221 | { 1222 | // x grows -4 to 4, y grows -1 to 1 1223 | float sum = 1224 | d[-4 - xs] + 1225 | d[-3 - xs] + 1226 | d[-2 - xs] + 1227 | d[-1] + 1228 | d[0] + 1229 | d[1] + 1230 | d[2 + xs] + 1231 | d[3 + xs] + 1232 | d[4 + xs]; 1233 | retval += sum * sum; 1234 | } 1235 | { 1236 | // x grows -4 to 4, y shrinks 1 to -1 1237 | float sum = 1238 | d[-4 + xs] + 1239 | d[-3 + xs] + 1240 | d[-2 + xs] + 1241 | d[-1] + 1242 | d[0] + 1243 | d[1] + 1244 | d[2 - xs] + 1245 | d[3 - xs] + 1246 | d[4 - xs]; 1247 | retval += sum * sum; 1248 | } 1249 | { 1250 | /* 0_________ 1251 | 1__*______ 1252 | 2___*_____ 1253 | 3___*_____ 1254 | 4____0____ 1255 | 5_____*___ 1256 | 6_____*___ 1257 | 7______*__ 1258 | 8_________ */ 1259 | float sum = 1260 | d[-xs3 - 2] + 1261 | d[-xs - xs - 1] + 1262 | d[-xs - 1] + 1263 | d[0] + 1264 | d[xs + 1] + 1265 | d[xs + xs + 1] + 1266 | d[xs3 + 2]; 1267 | retval += sum * sum; 1268 | } 1269 | { 1270 | /* 0_________ 1271 | 1______*__ 1272 | 2_____*___ 1273 | 3_____*___ 1274 | 4____0____ 1275 | 5___*_____ 1276 | 6___*_____ 1277 | 7__*______ 1278 | 8_________ */ 1279 | float sum = 1280 | d[-xs3 + 2] + 1281 | d[-xs - xs + 1] + 1282 | d[-xs + 1] + 1283 | d[0] + 1284 | d[xs - 1] + 1285 | d[xs + xs - 1] + 1286 | d[xs3 - 2]; 1287 | retval += sum * sum; 1288 | } 1289 | { 1290 | /* 0_________ 1291 | 1_________ 1292 | 2_*_______ 1293 | 3__**_____ 1294 | 4____0____ 1295 | 5_____**__ 1296 | 6_______*_ 1297 | 7_________ 1298 | 8_________ */ 1299 | float sum = 1300 | d[-xs - xs - 3] + 1301 | d[-xs - 2] + 1302 | d[-xs - 1] + 1303 | d[0] + 1304 | d[xs + 1] + 1305 | d[xs + 2] + 1306 | d[xs + xs + 3]; 1307 | retval += sum * sum; 1308 | } 1309 | { 1310 | /* 0_________ 1311 | 1_________ 1312 | 2_______*_ 1313 | 3_____**__ 1314 | 4____0____ 1315 | 5__**_____ 1316 | 6_*_______ 1317 | 7_________ 1318 | 8_________ */ 1319 | float sum = 1320 | d[-xs - xs + 3] + 1321 | d[-xs + 2] + 1322 | d[-xs + 1] + 1323 | d[0] + 1324 | d[xs - 1] + 1325 | d[xs - 2] + 1326 | d[xs + xs - 3]; 1327 | retval += sum * sum; 1328 | } 1329 | { 1330 | /* 0_________ 1331 | 1_________ 1332 | 2_________ 1333 | 3______**_ 1334 | 4____0*___ 1335 | 5__**_____ 1336 | 6**_______ 1337 | 7_________ 1338 | 8_________ */ 1339 | 1340 | float sum = 1341 | d[xs + xs - 4] + 1342 | d[xs + xs - 3] + 1343 | d[xs - 2] + 1344 | d[xs - 1] + 1345 | d[0] + 1346 | d[1] + 1347 | d[-xs + 2] + 1348 | d[-xs + 3]; 1349 | retval += sum * sum; 1350 | } 1351 | { 1352 | /* 0_________ 1353 | 1_________ 1354 | 2**_______ 1355 | 3__**_____ 1356 | 4____0*___ 1357 | 5______**_ 1358 | 6_________ 1359 | 7_________ 1360 | 8_________ */ 1361 | float sum = 1362 | d[-xs - xs - 4] + 1363 | d[-xs - xs - 3] + 1364 | d[-xs - 2] + 1365 | d[-xs - 1] + 1366 | d[0] + 1367 | d[1] + 1368 | d[xs + 2] + 1369 | d[xs + 3]; 1370 | retval += sum * sum; 1371 | } 1372 | { 1373 | /* 0__*______ 1374 | 1__*______ 1375 | 2___*_____ 1376 | 3___*_____ 1377 | 4____0____ 1378 | 5____*____ 1379 | 6_____*___ 1380 | 7_____*___ 1381 | 8_________ */ 1382 | float sum = 1383 | d[-xs3 - xs - 2] + 1384 | d[-xs3 - 2] + 1385 | d[-xs - xs - 1] + 1386 | d[-xs - 1] + 1387 | d[0] + 1388 | d[xs] + 1389 | d[xs + xs + 1] + 1390 | d[xs3 + 1]; 1391 | retval += sum * sum; 1392 | } 1393 | { 1394 | /* 0______*__ 1395 | 1______*__ 1396 | 2_____*___ 1397 | 3_____*___ 1398 | 4____0____ 1399 | 5____*____ 1400 | 6___*_____ 1401 | 7___*_____ 1402 | 8_________ */ 1403 | float sum = 1404 | d[-xs3 - xs + 2] + 1405 | d[-xs3 + 2] + 1406 | d[-xs - xs + 1] + 1407 | d[-xs + 1] + 1408 | d[0] + 1409 | d[xs] + 1410 | d[xs + xs - 1] + 1411 | d[xs3 - 1]; 1412 | retval += sum * sum; 1413 | } 1414 | return retval; 1415 | } 1416 | 1417 | // Returns MaltaUnit. "fastMode" avoids bounds-checks when x0 and y0 are known 1418 | // to be far enough from the image borders. 1419 | template 1420 | static BUTTERAUGLI_INLINE float PaddedMaltaUnit( 1421 | float* const BUTTERAUGLI_RESTRICT diffs, const size_t x0, const size_t y0, 1422 | const size_t xsize_, const size_t ysize_) { 1423 | int ix0 = y0 * xsize_ + x0; 1424 | const float* BUTTERAUGLI_RESTRICT d = &diffs[ix0]; 1425 | if (fastMode || 1426 | (x0 >= 4 && y0 >= 4 && x0 < (xsize_ - 4) && y0 < (ysize_ - 4))) { 1427 | return MaltaUnit(Tag(), d, xsize_); 1428 | } 1429 | 1430 | float borderimage[9 * 9]; 1431 | for (int dy = 0; dy < 9; ++dy) { 1432 | int y = y0 + dy - 4; 1433 | if (y < 0 || y >= ysize_) { 1434 | for (int dx = 0; dx < 9; ++dx) { 1435 | borderimage[dy * 9 + dx] = 0.0f; 1436 | } 1437 | } else { 1438 | for (int dx = 0; dx < 9; ++dx) { 1439 | int x = x0 + dx - 4; 1440 | if (x < 0 || x >= xsize_) { 1441 | borderimage[dy * 9 + dx] = 0.0f; 1442 | } else { 1443 | borderimage[dy * 9 + dx] = diffs[y * xsize_ + x]; 1444 | } 1445 | } 1446 | } 1447 | } 1448 | return MaltaUnit(Tag(), &borderimage[4 * 9 + 4], 9); 1449 | } 1450 | 1451 | template 1452 | static void MaltaDiffMapImpl(const ImageF& lum0, const ImageF& lum1, 1453 | const size_t xsize_, const size_t ysize_, 1454 | const double w_0gt1, 1455 | const double w_0lt1, 1456 | double norm1, 1457 | const double len, const double mulli, 1458 | ImageF* block_diff_ac) { 1459 | const float kWeight0 = 0.5; 1460 | const float kWeight1 = 0.33; 1461 | 1462 | const double w_pre0gt1 = mulli * sqrt(kWeight0 * w_0gt1) / (len * 2 + 1); 1463 | const double w_pre0lt1 = mulli * sqrt(kWeight1 * w_0lt1) / (len * 2 + 1); 1464 | const float norm2_0gt1 = w_pre0gt1 * norm1; 1465 | const float norm2_0lt1 = w_pre0lt1 * norm1; 1466 | 1467 | std::vector diffs(ysize_ * xsize_); 1468 | for (size_t y = 0, ix = 0; y < ysize_; ++y) { 1469 | const float* BUTTERAUGLI_RESTRICT const row0 = lum0.Row(y); 1470 | const float* BUTTERAUGLI_RESTRICT const row1 = lum1.Row(y); 1471 | for (size_t x = 0; x < xsize_; ++x, ++ix) { 1472 | const float absval = 0.5 * std::abs(row0[x]) + 0.5 * std::abs(row1[x]); 1473 | const float diff = row0[x] - row1[x]; 1474 | const float scaler = norm2_0gt1 / (static_cast(norm1) + absval); 1475 | 1476 | // Primary symmetric quadratic objective. 1477 | diffs[ix] = scaler * diff; 1478 | 1479 | const float scaler2 = norm2_0lt1 / (static_cast(norm1) + absval); 1480 | const double fabs0 = fabs(row0[x]); 1481 | 1482 | // Secondary half-open quadratic objectives. 1483 | const double too_small = 0.55 * fabs0; 1484 | const double too_big = 1.05 * fabs0; 1485 | 1486 | if (row0[x] < 0) { 1487 | if (row1[x] > -too_small) { 1488 | double impact = scaler2 * (row1[x] + too_small); 1489 | if (diff < 0) { 1490 | diffs[ix] -= impact; 1491 | } else { 1492 | diffs[ix] += impact; 1493 | } 1494 | } else if (row1[x] < -too_big) { 1495 | double impact = scaler2 * (-row1[x] - too_big); 1496 | if (diff < 0) { 1497 | diffs[ix] -= impact; 1498 | } else { 1499 | diffs[ix] += impact; 1500 | } 1501 | } 1502 | } else { 1503 | if (row1[x] < too_small) { 1504 | double impact = scaler2 * (too_small - row1[x]); 1505 | if (diff < 0) { 1506 | diffs[ix] -= impact; 1507 | } else { 1508 | diffs[ix] += impact; 1509 | } 1510 | } else if (row1[x] > too_big) { 1511 | double impact = scaler2 * (row1[x] - too_big); 1512 | if (diff < 0) { 1513 | diffs[ix] -= impact; 1514 | } else { 1515 | diffs[ix] += impact; 1516 | } 1517 | } 1518 | } 1519 | } 1520 | } 1521 | 1522 | size_t y0 = 0; 1523 | // Top 1524 | for (; y0 < 4; ++y0) { 1525 | float* const BUTTERAUGLI_RESTRICT row_diff = block_diff_ac->Row(y0); 1526 | for (size_t x0 = 0; x0 < xsize_; ++x0) { 1527 | row_diff[x0] += 1528 | PaddedMaltaUnit(&diffs[0], x0, y0, xsize_, ysize_); 1529 | } 1530 | } 1531 | 1532 | // Middle 1533 | for (; y0 < ysize_ - 4; ++y0) { 1534 | float* const BUTTERAUGLI_RESTRICT row_diff = block_diff_ac->Row(y0); 1535 | size_t x0 = 0; 1536 | for (; x0 < 4; ++x0) { 1537 | row_diff[x0] += 1538 | PaddedMaltaUnit(&diffs[0], x0, y0, xsize_, ysize_); 1539 | } 1540 | for (; x0 < xsize_ - 4; ++x0) { 1541 | row_diff[x0] += 1542 | PaddedMaltaUnit(&diffs[0], x0, y0, xsize_, ysize_); 1543 | } 1544 | 1545 | for (; x0 < xsize_; ++x0) { 1546 | row_diff[x0] += 1547 | PaddedMaltaUnit(&diffs[0], x0, y0, xsize_, ysize_); 1548 | } 1549 | } 1550 | 1551 | // Bottom 1552 | for (; y0 < ysize_; ++y0) { 1553 | float* const BUTTERAUGLI_RESTRICT row_diff = block_diff_ac->Row(y0); 1554 | for (size_t x0 = 0; x0 < xsize_; ++x0) { 1555 | row_diff[x0] += 1556 | PaddedMaltaUnit(&diffs[0], x0, y0, xsize_, ysize_); 1557 | } 1558 | } 1559 | } 1560 | 1561 | void ButteraugliComparator::MaltaDiffMap( 1562 | const ImageF& lum0, const ImageF& lum1, 1563 | const double w_0gt1, 1564 | const double w_0lt1, 1565 | const double norm1, ImageF* BUTTERAUGLI_RESTRICT block_diff_ac) const { 1566 | PROFILER_FUNC; 1567 | const double len = 3.75; 1568 | static const double mulli = 0.354191303559; 1569 | MaltaDiffMapImpl(lum0, lum1, xsize_, ysize_, w_0gt1, w_0lt1, 1570 | norm1, len, 1571 | mulli, block_diff_ac); 1572 | } 1573 | 1574 | void ButteraugliComparator::MaltaDiffMapLF( 1575 | const ImageF& lum0, const ImageF& lum1, 1576 | const double w_0gt1, 1577 | const double w_0lt1, 1578 | const double norm1, ImageF* BUTTERAUGLI_RESTRICT block_diff_ac) const { 1579 | PROFILER_FUNC; 1580 | const double len = 3.75; 1581 | static const double mulli = 0.405371989604; 1582 | MaltaDiffMapImpl(lum0, lum1, xsize_, ysize_, 1583 | w_0gt1, w_0lt1, 1584 | norm1, len, 1585 | mulli, block_diff_ac); 1586 | } 1587 | 1588 | ImageF ButteraugliComparator::CombineChannels( 1589 | const std::vector& mask_xyb, 1590 | const std::vector& mask_xyb_dc, 1591 | const std::vector& block_diff_dc, 1592 | const std::vector& block_diff_ac) const { 1593 | PROFILER_FUNC; 1594 | ImageF result(xsize_, ysize_); 1595 | for (size_t y = 0; y < ysize_; ++y) { 1596 | float* const BUTTERAUGLI_RESTRICT row_out = result.Row(y); 1597 | for (size_t x = 0; x < xsize_; ++x) { 1598 | float mask[3]; 1599 | float dc_mask[3]; 1600 | float diff_dc[3]; 1601 | float diff_ac[3]; 1602 | for (int i = 0; i < 3; ++i) { 1603 | mask[i] = mask_xyb[i].Row(y)[x]; 1604 | dc_mask[i] = mask_xyb_dc[i].Row(y)[x]; 1605 | diff_dc[i] = block_diff_dc[i].Row(y)[x]; 1606 | diff_ac[i] = block_diff_ac[i].Row(y)[x]; 1607 | } 1608 | row_out[x] = (DotProduct(diff_dc, dc_mask) + DotProduct(diff_ac, mask)); 1609 | } 1610 | } 1611 | return result; 1612 | } 1613 | 1614 | double ButteraugliScoreFromDiffmap(const ImageF& diffmap) { 1615 | PROFILER_FUNC; 1616 | float retval = 0.0f; 1617 | for (size_t y = 0; y < diffmap.ysize(); ++y) { 1618 | const float * const BUTTERAUGLI_RESTRICT row = diffmap.Row(y); 1619 | for (size_t x = 0; x < diffmap.xsize(); ++x) { 1620 | retval = std::max(retval, row[x]); 1621 | } 1622 | } 1623 | return retval; 1624 | } 1625 | 1626 | #include 1627 | 1628 | // ===== Functions used by Mask only ===== 1629 | static std::array MakeMask( 1630 | double extmul, double extoff, 1631 | double mul, double offset, 1632 | double scaler) { 1633 | std::array lut; 1634 | for (int i = 0; i < lut.size(); ++i) { 1635 | const double c = mul / ((0.01 * scaler * i) + offset); 1636 | lut[i] = kGlobalScale * (1.0 + extmul * (c + extoff)); 1637 | if (lut[i] < 1e-5) { 1638 | lut[i] = 1e-5; 1639 | } 1640 | assert(lut[i] >= 0.0); 1641 | lut[i] *= lut[i]; 1642 | } 1643 | return lut; 1644 | } 1645 | 1646 | double MaskX(double delta) { 1647 | static const double extmul = 2.59885507073; 1648 | static const double extoff = 3.08805636789; 1649 | static const double offset = 0.315424196682; 1650 | static const double scaler = 16.2770141832; 1651 | static const double mul = 5.62939030582; 1652 | static const std::array lut = 1653 | MakeMask(extmul, extoff, mul, offset, scaler); 1654 | return InterpolateClampNegative(lut.data(), lut.size(), delta); 1655 | } 1656 | 1657 | double MaskY(double delta) { 1658 | static const double extmul = 0.9613705131; 1659 | static const double extoff = -0.581933100068; 1660 | static const double offset = 1.00846207765; 1661 | static const double scaler = 2.2342321176; 1662 | static const double mul = 6.64307621174; 1663 | static const std::array lut = 1664 | MakeMask(extmul, extoff, mul, offset, scaler); 1665 | return InterpolateClampNegative(lut.data(), lut.size(), delta); 1666 | } 1667 | 1668 | double MaskDcX(double delta) { 1669 | static const double extmul = 10.0470705878; 1670 | static const double extoff = 3.18472654033; 1671 | static const double offset = 0.0551512255218; 1672 | static const double scaler = 70.0; 1673 | static const double mul = 0.373092999662; 1674 | static const std::array lut = 1675 | MakeMask(extmul, extoff, mul, offset, scaler); 1676 | return InterpolateClampNegative(lut.data(), lut.size(), delta); 1677 | } 1678 | 1679 | double MaskDcY(double delta) { 1680 | static const double extmul = 0.0115640939227; 1681 | static const double extoff = 45.9483175519; 1682 | static const double offset = 0.0142290066313; 1683 | static const double scaler = 5.0; 1684 | static const double mul = 2.52611324247; 1685 | static const std::array lut = 1686 | MakeMask(extmul, extoff, mul, offset, scaler); 1687 | return InterpolateClampNegative(lut.data(), lut.size(), delta); 1688 | } 1689 | 1690 | ImageF DiffPrecompute(const ImageF& xyb0, const ImageF& xyb1) { 1691 | PROFILER_FUNC; 1692 | const size_t xsize = xyb0.xsize(); 1693 | const size_t ysize = xyb0.ysize(); 1694 | ImageF result(xsize, ysize); 1695 | size_t x2, y2; 1696 | for (size_t y = 0; y < ysize; ++y) { 1697 | if (y + 1 < ysize) { 1698 | y2 = y + 1; 1699 | } else if (y > 0) { 1700 | y2 = y - 1; 1701 | } else { 1702 | y2 = y; 1703 | } 1704 | const float* const BUTTERAUGLI_RESTRICT row0_in = xyb0.Row(y); 1705 | const float* const BUTTERAUGLI_RESTRICT row1_in = xyb1.Row(y); 1706 | const float* const BUTTERAUGLI_RESTRICT row0_in2 = xyb0.Row(y2); 1707 | const float* const BUTTERAUGLI_RESTRICT row1_in2 = xyb1.Row(y2); 1708 | float* const BUTTERAUGLI_RESTRICT row_out = result.Row(y); 1709 | for (size_t x = 0; x < xsize; ++x) { 1710 | if (x + 1 < xsize) { 1711 | x2 = x + 1; 1712 | } else if (x > 0) { 1713 | x2 = x - 1; 1714 | } else { 1715 | x2 = x; 1716 | } 1717 | double sup0 = (fabs(row0_in[x] - row0_in[x2]) + 1718 | fabs(row0_in[x] - row0_in2[x])); 1719 | double sup1 = (fabs(row1_in[x] - row1_in[x2]) + 1720 | fabs(row1_in[x] - row1_in2[x])); 1721 | static const double mul0 = 0.918416534734; 1722 | row_out[x] = mul0 * std::min(sup0, sup1); 1723 | static const double cutoff = 55.0184555849; 1724 | if (row_out[x] >= cutoff) { 1725 | row_out[x] = cutoff; 1726 | } 1727 | } 1728 | } 1729 | return result; 1730 | } 1731 | 1732 | void Mask(const std::vector& xyb0, 1733 | const std::vector& xyb1, 1734 | std::vector* BUTTERAUGLI_RESTRICT mask, 1735 | std::vector* BUTTERAUGLI_RESTRICT mask_dc) { 1736 | PROFILER_FUNC; 1737 | const size_t xsize = xyb0[0].xsize(); 1738 | const size_t ysize = xyb0[0].ysize(); 1739 | mask->resize(3); 1740 | *mask_dc = CreatePlanes(xsize, ysize, 3); 1741 | double muls[2] = { 1742 | 0.207017089891, 1743 | 0.267138152891, 1744 | }; 1745 | double normalizer = { 1746 | 1.0 / (muls[0] + muls[1]), 1747 | }; 1748 | static const double r0 = 2.3770330432; 1749 | static const double r1 = 9.04353323561; 1750 | static const double r2 = 9.24456601467; 1751 | static const double border_ratio = -0.0724948220913; 1752 | 1753 | { 1754 | // X component 1755 | ImageF diff = DiffPrecompute(xyb0[0], xyb1[0]); 1756 | ImageF blurred = Blur(diff, r2, border_ratio); 1757 | (*mask)[0] = ImageF(xsize, ysize); 1758 | for (size_t y = 0; y < ysize; ++y) { 1759 | for (size_t x = 0; x < xsize; ++x) { 1760 | (*mask)[0].Row(y)[x] = blurred.Row(y)[x]; 1761 | } 1762 | } 1763 | } 1764 | { 1765 | // Y component 1766 | (*mask)[1] = ImageF(xsize, ysize); 1767 | ImageF diff = DiffPrecompute(xyb0[1], xyb1[1]); 1768 | ImageF blurred1 = Blur(diff, r0, border_ratio); 1769 | ImageF blurred2 = Blur(diff, r1, border_ratio); 1770 | for (size_t y = 0; y < ysize; ++y) { 1771 | for (size_t x = 0; x < xsize; ++x) { 1772 | const double val = normalizer * ( 1773 | muls[0] * blurred1.Row(y)[x] + 1774 | muls[1] * blurred2.Row(y)[x]); 1775 | (*mask)[1].Row(y)[x] = val; 1776 | } 1777 | } 1778 | } 1779 | // B component 1780 | (*mask)[2] = ImageF(xsize, ysize); 1781 | static const double mul[2] = { 1782 | 16.6963293877, 1783 | 2.1364621982, 1784 | }; 1785 | static const double w00 = 36.4671237619; 1786 | static const double w11 = 2.1887170895; 1787 | static const double w_ytob_hf = std::max( 1788 | 0.086624184478, 1789 | 0.0); 1790 | static const double w_ytob_lf = 21.6804277046; 1791 | static const double p1_to_p0 = 0.0513061271723; 1792 | 1793 | for (size_t y = 0; y < ysize; ++y) { 1794 | for (size_t x = 0; x < xsize; ++x) { 1795 | const double s0 = (*mask)[0].Row(y)[x]; 1796 | const double s1 = (*mask)[1].Row(y)[x]; 1797 | const double p1 = mul[1] * w11 * s1; 1798 | const double p0 = mul[0] * w00 * s0 + p1_to_p0 * p1; 1799 | 1800 | (*mask)[0].Row(y)[x] = MaskX(p0); 1801 | (*mask)[1].Row(y)[x] = MaskY(p1); 1802 | (*mask)[2].Row(y)[x] = w_ytob_hf * MaskY(p1); 1803 | (*mask_dc)[0].Row(y)[x] = MaskDcX(p0); 1804 | (*mask_dc)[1].Row(y)[x] = MaskDcY(p1); 1805 | (*mask_dc)[2].Row(y)[x] = w_ytob_lf * MaskDcY(p1); 1806 | } 1807 | } 1808 | } 1809 | 1810 | void ButteraugliDiffmap(const std::vector &rgb0_image, 1811 | const std::vector &rgb1_image, 1812 | double hf_asymmetry, 1813 | ImageF &result_image) { 1814 | PROFILER_FUNC; 1815 | const size_t xsize = rgb0_image[0].xsize(); 1816 | const size_t ysize = rgb0_image[0].ysize(); 1817 | static const int kMax = 8; 1818 | if (xsize < kMax || ysize < kMax) { 1819 | // Butteraugli values for small (where xsize or ysize is smaller 1820 | // than 8 pixels) images are non-sensical, but most likely it is 1821 | // less disruptive to try to compute something than just give up. 1822 | // Temporarily extend the borders of the image to fit 8 x 8 size. 1823 | int xborder = xsize < kMax ? (kMax - xsize) / 2 : 0; 1824 | int yborder = ysize < kMax ? (kMax - ysize) / 2 : 0; 1825 | size_t xscaled = std::max(kMax, xsize); 1826 | size_t yscaled = std::max(kMax, ysize); 1827 | std::vector scaled0 = CreatePlanes(xscaled, yscaled, 3); 1828 | std::vector scaled1 = CreatePlanes(xscaled, yscaled, 3); 1829 | for (int i = 0; i < 3; ++i) { 1830 | for (int y = 0; y < yscaled; ++y) { 1831 | for (int x = 0; x < xscaled; ++x) { 1832 | size_t x2 = std::min(xsize - 1, std::max(0, x - xborder)); 1833 | size_t y2 = std::min(ysize - 1, std::max(0, y - yborder)); 1834 | scaled0[i].Row(y)[x] = rgb0_image[i].Row(y2)[x2]; 1835 | scaled1[i].Row(y)[x] = rgb1_image[i].Row(y2)[x2]; 1836 | } 1837 | } 1838 | } 1839 | ImageF diffmap_scaled; 1840 | ButteraugliDiffmap(scaled0, scaled1, hf_asymmetry, diffmap_scaled); 1841 | result_image = ImageF(xsize, ysize); 1842 | for (int y = 0; y < ysize; ++y) { 1843 | for (int x = 0; x < xsize; ++x) { 1844 | result_image.Row(y)[x] = diffmap_scaled.Row(y + yborder)[x + xborder]; 1845 | } 1846 | } 1847 | return; 1848 | } 1849 | ButteraugliComparator butteraugli(rgb0_image, hf_asymmetry); 1850 | butteraugli.Diffmap(rgb1_image, result_image); 1851 | } 1852 | 1853 | bool ButteraugliInterface(const std::vector &rgb0, 1854 | const std::vector &rgb1, 1855 | float hf_asymmetry, 1856 | ImageF &diffmap, 1857 | double &diffvalue) { 1858 | const size_t xsize = rgb0[0].xsize(); 1859 | const size_t ysize = rgb0[0].ysize(); 1860 | if (xsize < 1 || ysize < 1) { 1861 | return false; // No image. 1862 | } 1863 | for (int i = 1; i < 3; i++) { 1864 | if (rgb0[i].xsize() != xsize || rgb0[i].ysize() != ysize || 1865 | rgb1[i].xsize() != xsize || rgb1[i].ysize() != ysize) { 1866 | return false; // Image planes must have same dimensions. 1867 | } 1868 | } 1869 | ButteraugliDiffmap(rgb0, rgb1, hf_asymmetry, diffmap); 1870 | diffvalue = ButteraugliScoreFromDiffmap(diffmap); 1871 | return true; 1872 | } 1873 | 1874 | bool ButteraugliAdaptiveQuantization(size_t xsize, size_t ysize, 1875 | const std::vector > &rgb, std::vector &quant) { 1876 | if (xsize < 16 || ysize < 16) { 1877 | return false; // Butteraugli is undefined for small images. 1878 | } 1879 | size_t size = xsize * ysize; 1880 | 1881 | std::vector rgb_planes = PlanesFromPacked(xsize, ysize, rgb); 1882 | std::vector scale_xyb; 1883 | std::vector scale_xyb_dc; 1884 | Mask(rgb_planes, rgb_planes, &scale_xyb, &scale_xyb_dc); 1885 | quant.reserve(size); 1886 | 1887 | // Mask gives us values in 3 color channels, but for now we take only 1888 | // the intensity channel. 1889 | for (size_t y = 0; y < ysize; ++y) { 1890 | for (size_t x = 0; x < xsize; ++x) { 1891 | quant.push_back(scale_xyb[1].Row(y)[x]); 1892 | } 1893 | } 1894 | return true; 1895 | } 1896 | 1897 | double ButteraugliFuzzyClass(double score) { 1898 | static const double fuzzy_width_up = 6.07887388532; 1899 | static const double fuzzy_width_down = 5.50793514384; 1900 | static const double m0 = 2.0; 1901 | static const double scaler = 0.840253347958; 1902 | double val; 1903 | if (score < 1.0) { 1904 | // val in [scaler .. 2.0] 1905 | val = m0 / (1.0 + exp((score - 1.0) * fuzzy_width_down)); 1906 | val -= 1.0; // from [1 .. 2] to [0 .. 1] 1907 | val *= 2.0 - scaler; // from [0 .. 1] to [0 .. 2.0 - scaler] 1908 | val += scaler; // from [0 .. 2.0 - scaler] to [scaler .. 2.0] 1909 | } else { 1910 | // val in [0 .. scaler] 1911 | val = m0 / (1.0 + exp((score - 1.0) * fuzzy_width_up)); 1912 | val *= scaler; 1913 | } 1914 | return val; 1915 | } 1916 | 1917 | double ButteraugliFuzzyInverse(double seek) { 1918 | double pos = 0; 1919 | for (double range = 1.0; range >= 1e-10; range *= 0.5) { 1920 | double cur = ButteraugliFuzzyClass(pos); 1921 | if (cur < seek) { 1922 | pos -= range; 1923 | } else { 1924 | pos += range; 1925 | } 1926 | } 1927 | return pos; 1928 | } 1929 | 1930 | namespace { 1931 | 1932 | void ScoreToRgb(double score, double good_threshold, double bad_threshold, 1933 | uint8_t rgb[3]) { 1934 | double heatmap[12][3] = { 1935 | {0, 0, 0}, 1936 | {0, 0, 1}, 1937 | {0, 1, 1}, 1938 | {0, 1, 0}, // Good level 1939 | {1, 1, 0}, 1940 | {1, 0, 0}, // Bad level 1941 | {1, 0, 1}, 1942 | {0.5, 0.5, 1.0}, 1943 | {1.0, 0.5, 0.5}, // Pastel colors for the very bad quality range. 1944 | {1.0, 1.0, 0.5}, 1945 | { 1946 | 1, 1, 1, 1947 | }, 1948 | { 1949 | 1, 1, 1, 1950 | }, 1951 | }; 1952 | if (score < good_threshold) { 1953 | score = (score / good_threshold) * 0.3; 1954 | } else if (score < bad_threshold) { 1955 | score = 0.3 + 1956 | (score - good_threshold) / (bad_threshold - good_threshold) * 0.15; 1957 | } else { 1958 | score = 0.45 + (score - bad_threshold) / (bad_threshold * 12) * 0.5; 1959 | } 1960 | static const int kTableSize = sizeof(heatmap) / sizeof(heatmap[0]); 1961 | score = std::min(std::max(score * (kTableSize - 1), 0.0), 1962 | kTableSize - 2); 1963 | int ix = static_cast(score); 1964 | double mix = score - ix; 1965 | for (int i = 0; i < 3; ++i) { 1966 | double v = mix * heatmap[ix + 1][i] + (1 - mix) * heatmap[ix][i]; 1967 | rgb[i] = static_cast(255 * pow(v, 0.5) + 0.5); 1968 | } 1969 | } 1970 | 1971 | } // namespace 1972 | 1973 | void CreateHeatMapImage(const std::vector& distmap, 1974 | double good_threshold, double bad_threshold, 1975 | size_t xsize, size_t ysize, 1976 | std::vector* heatmap) { 1977 | heatmap->resize(3 * xsize * ysize); 1978 | for (size_t y = 0; y < ysize; ++y) { 1979 | for (size_t x = 0; x < xsize; ++x) { 1980 | int px = xsize * y + x; 1981 | double d = distmap[px]; 1982 | uint8_t* rgb = &(*heatmap)[3 * px]; 1983 | ScoreToRgb(d, good_threshold, bad_threshold, rgb); 1984 | } 1985 | } 1986 | } 1987 | 1988 | } // namespace butteraugli 1989 | -------------------------------------------------------------------------------- /butteraugli/butteraugli.h: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Google Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | // Disclaimer: This is not an official Google product. 16 | // 17 | // Author: Jyrki Alakuijala (jyrki.alakuijala@gmail.com) 18 | 19 | #ifndef BUTTERAUGLI_BUTTERAUGLI_H_ 20 | #define BUTTERAUGLI_BUTTERAUGLI_H_ 21 | 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | 31 | #define BUTTERAUGLI_ENABLE_CHECKS 0 32 | 33 | // This is the main interface to butteraugli image similarity 34 | // analysis function. 35 | 36 | namespace butteraugli { 37 | 38 | template 39 | class Image; 40 | 41 | using Image8 = Image; 42 | using ImageF = Image; 43 | 44 | // ButteraugliInterface defines the public interface for butteraugli. 45 | // 46 | // It calculates the difference between rgb0 and rgb1. 47 | // 48 | // rgb0 and rgb1 contain the images. rgb0[c][px] and rgb1[c][px] contains 49 | // the red image for c == 0, green for c == 1, blue for c == 2. Location index 50 | // px is calculated as y * xsize + x. 51 | // 52 | // Value of pixels of images rgb0 and rgb1 need to be represented as raw 53 | // intensity. Most image formats store gamma corrected intensity in pixel 54 | // values. This gamma correction has to be removed, by applying the following 55 | // function: 56 | // butteraugli_val = 255.0 * pow(png_val / 255.0, gamma); 57 | // A typical value of gamma is 2.2. It is usually stored in the image header. 58 | // Take care not to confuse that value with its inverse. The gamma value should 59 | // be always greater than one. 60 | // Butteraugli does not work as intended if the caller does not perform 61 | // gamma correction. 62 | // 63 | // diffmap will contain an image of the size xsize * ysize, containing 64 | // localized differences for values px (indexed with the px the same as rgb0 65 | // and rgb1). diffvalue will give a global score of similarity. 66 | // 67 | // A diffvalue smaller than kButteraugliGood indicates that images can be 68 | // observed as the same image. 69 | // diffvalue larger than kButteraugliBad indicates that a difference between 70 | // the images can be observed. 71 | // A diffvalue between kButteraugliGood and kButteraugliBad indicates that 72 | // a subtle difference can be observed between the images. 73 | // 74 | // Returns true on success. 75 | 76 | bool ButteraugliInterface(const std::vector &rgb0, 77 | const std::vector &rgb1, 78 | float hf_asymmetry, 79 | ImageF &diffmap, 80 | double &diffvalue); 81 | 82 | const double kButteraugliQuantLow = 0.26; 83 | const double kButteraugliQuantHigh = 1.454; 84 | 85 | // Converts the butteraugli score into fuzzy class values that are continuous 86 | // at the class boundary. The class boundary location is based on human 87 | // raters, but the slope is arbitrary. Particularly, it does not reflect 88 | // the expectation value of probabilities of the human raters. It is just 89 | // expected that a smoother class boundary will allow for higher-level 90 | // optimization algorithms to work faster. 91 | // 92 | // Returns 2.0 for a perfect match, and 1.0 for 'ok', 0.0 for bad. Because the 93 | // scoring is fuzzy, a butteraugli score of 0.96 would return a class of 94 | // around 1.9. 95 | double ButteraugliFuzzyClass(double score); 96 | 97 | // Input values should be in range 0 (bad) to 2 (good). Use 98 | // kButteraugliNormalization as normalization. 99 | double ButteraugliFuzzyInverse(double seek); 100 | 101 | // Returns a map which can be used for adaptive quantization. Values can 102 | // typically range from kButteraugliQuantLow to kButteraugliQuantHigh. Low 103 | // values require coarse quantization (e.g. near random noise), high values 104 | // require fine quantization (e.g. in smooth bright areas). 105 | bool ButteraugliAdaptiveQuantization(size_t xsize, size_t ysize, 106 | const std::vector > &rgb, std::vector &quant); 107 | 108 | // Implementation details, don't use anything below or your code will 109 | // break in the future. 110 | 111 | #ifdef _MSC_VER 112 | #define BUTTERAUGLI_RESTRICT __restrict 113 | #else 114 | #define BUTTERAUGLI_RESTRICT __restrict__ 115 | #endif 116 | 117 | #ifdef _MSC_VER 118 | #define BUTTERAUGLI_INLINE __forceinline 119 | #else 120 | #define BUTTERAUGLI_INLINE inline 121 | #endif 122 | 123 | #ifdef __clang__ 124 | // Early versions of Clang did not support __builtin_assume_aligned. 125 | #define BUTTERAUGLI_HAS_ASSUME_ALIGNED __has_builtin(__builtin_assume_aligned) 126 | #elif defined(__GNUC__) 127 | #define BUTTERAUGLI_HAS_ASSUME_ALIGNED 1 128 | #else 129 | #define BUTTERAUGLI_HAS_ASSUME_ALIGNED 0 130 | #endif 131 | 132 | // Returns a void* pointer which the compiler then assumes is N-byte aligned. 133 | // Example: float* PIK_RESTRICT aligned = (float*)PIK_ASSUME_ALIGNED(in, 32); 134 | // 135 | // The assignment semantics are required by GCC/Clang. ICC provides an in-place 136 | // __assume_aligned, whereas MSVC's __assume appears unsuitable. 137 | #if BUTTERAUGLI_HAS_ASSUME_ALIGNED 138 | #define BUTTERAUGLI_ASSUME_ALIGNED(ptr, align) __builtin_assume_aligned((ptr), (align)) 139 | #else 140 | #define BUTTERAUGLI_ASSUME_ALIGNED(ptr, align) (ptr) 141 | #endif // BUTTERAUGLI_HAS_ASSUME_ALIGNED 142 | 143 | // Functions that depend on the cache line size. 144 | class CacheAligned { 145 | public: 146 | static constexpr size_t kPointerSize = sizeof(void *); 147 | static constexpr size_t kCacheLineSize = 64; 148 | 149 | // The aligned-return annotation is only allowed on function declarations. 150 | static void *Allocate(const size_t bytes); 151 | static void Free(void *aligned_pointer); 152 | }; 153 | 154 | template 155 | using CacheAlignedUniquePtrT = std::unique_ptr; 156 | 157 | using CacheAlignedUniquePtr = CacheAlignedUniquePtrT; 158 | 159 | template 160 | static inline CacheAlignedUniquePtrT Allocate(const size_t entries) { 161 | return CacheAlignedUniquePtrT( 162 | static_cast( 163 | CacheAligned::Allocate(entries * sizeof(T))), 164 | CacheAligned::Free); 165 | } 166 | 167 | // Returns the smallest integer not less than "amount" that is divisible by 168 | // "multiple", which must be a power of two. 169 | template 170 | static inline size_t Align(const size_t amount) { 171 | static_assert(multiple != 0 && ((multiple & (multiple - 1)) == 0), 172 | "Align<> argument must be a power of two"); 173 | return (amount + multiple - 1) & ~(multiple - 1); 174 | } 175 | 176 | // Single channel, contiguous (cache-aligned) rows separated by padding. 177 | // T must be POD. 178 | // 179 | // Rationale: vectorization benefits from aligned operands - unaligned loads and 180 | // especially stores are expensive when the address crosses cache line 181 | // boundaries. Introducing padding after each row ensures the start of a row is 182 | // aligned, and that row loops can process entire vectors (writes to the padding 183 | // are allowed and ignored). 184 | // 185 | // We prefer a planar representation, where channels are stored as separate 186 | // 2D arrays, because that simplifies vectorization (repeating the same 187 | // operation on multiple adjacent components) without the complexity of a 188 | // hybrid layout (8 R, 8 G, 8 B, ...). In particular, clients can easily iterate 189 | // over all components in a row and Image requires no knowledge of the pixel 190 | // format beyond the component type "T". The downside is that we duplicate the 191 | // xsize/ysize members for each channel. 192 | // 193 | // This image layout could also be achieved with a vector and a row accessor 194 | // function, but a class wrapper with support for "deleter" allows wrapping 195 | // existing memory allocated by clients without copying the pixels. It also 196 | // provides convenient accessors for xsize/ysize, which shortens function 197 | // argument lists. Supports move-construction so it can be stored in containers. 198 | template 199 | class Image { 200 | // Returns cache-aligned row stride, being careful to avoid 2K aliasing. 201 | static size_t BytesPerRow(const size_t xsize) { 202 | // Allow reading one extra AVX-2 vector on the right margin. 203 | const size_t row_size = xsize * sizeof(T) + 32; 204 | const size_t align = CacheAligned::kCacheLineSize; 205 | size_t bytes_per_row = (row_size + align - 1) & ~(align - 1); 206 | // During the lengthy window before writes are committed to memory, CPUs 207 | // guard against read after write hazards by checking the address, but 208 | // only the lower 11 bits. We avoid a false dependency between writes to 209 | // consecutive rows by ensuring their sizes are not multiples of 2 KiB. 210 | if (bytes_per_row % 2048 == 0) { 211 | bytes_per_row += align; 212 | } 213 | return bytes_per_row; 214 | } 215 | 216 | public: 217 | using T = ComponentType; 218 | 219 | Image() : xsize_(0), ysize_(0), bytes_per_row_(0), bytes_(nullptr, Ignore) {} 220 | 221 | Image(const size_t xsize, const size_t ysize) 222 | : xsize_(xsize), 223 | ysize_(ysize), 224 | bytes_per_row_(BytesPerRow(xsize)), 225 | bytes_(Allocate(bytes_per_row_ * ysize)) {} 226 | 227 | Image(const size_t xsize, const size_t ysize, T val) 228 | : xsize_(xsize), 229 | ysize_(ysize), 230 | bytes_per_row_(BytesPerRow(xsize)), 231 | bytes_(Allocate(bytes_per_row_ * ysize)) { 232 | for (size_t y = 0; y < ysize_; ++y) { 233 | T* const BUTTERAUGLI_RESTRICT row = Row(y); 234 | for (int x = 0; x < xsize_; ++x) { 235 | row[x] = val; 236 | } 237 | } 238 | } 239 | 240 | Image(const size_t xsize, const size_t ysize, 241 | uint8_t * const BUTTERAUGLI_RESTRICT bytes, 242 | const size_t bytes_per_row) 243 | : xsize_(xsize), 244 | ysize_(ysize), 245 | bytes_per_row_(bytes_per_row), 246 | bytes_(bytes, Ignore) {} 247 | 248 | // Move constructor (required for returning Image from function) 249 | Image(Image &&other) 250 | : xsize_(other.xsize_), 251 | ysize_(other.ysize_), 252 | bytes_per_row_(other.bytes_per_row_), 253 | bytes_(std::move(other.bytes_)) {} 254 | 255 | // Move assignment (required for std::vector) 256 | Image &operator=(Image &&other) { 257 | xsize_ = other.xsize_; 258 | ysize_ = other.ysize_; 259 | bytes_per_row_ = other.bytes_per_row_; 260 | bytes_ = std::move(other.bytes_); 261 | return *this; 262 | } 263 | 264 | void Swap(Image &other) { 265 | std::swap(xsize_, other.xsize_); 266 | std::swap(ysize_, other.ysize_); 267 | std::swap(bytes_per_row_, other.bytes_per_row_); 268 | std::swap(bytes_, other.bytes_); 269 | } 270 | 271 | // How many pixels. 272 | size_t xsize() const { return xsize_; } 273 | size_t ysize() const { return ysize_; } 274 | 275 | T *const BUTTERAUGLI_RESTRICT Row(const size_t y) { 276 | #ifdef BUTTERAUGLI_ENABLE_CHECKS 277 | if (y >= ysize_) { 278 | printf("Row %zu out of bounds (ysize=%zu)\n", y, ysize_); 279 | abort(); 280 | } 281 | #endif 282 | void *row = bytes_.get() + y * bytes_per_row_; 283 | return reinterpret_cast(BUTTERAUGLI_ASSUME_ALIGNED(row, 64)); 284 | } 285 | 286 | const T *const BUTTERAUGLI_RESTRICT Row(const size_t y) const { 287 | #ifdef BUTTERAUGLI_ENABLE_CHECKS 288 | if (y >= ysize_) { 289 | printf("Const row %zu out of bounds (ysize=%zu)\n", y, ysize_); 290 | abort(); 291 | } 292 | #endif 293 | void *row = bytes_.get() + y * bytes_per_row_; 294 | return reinterpret_cast(BUTTERAUGLI_ASSUME_ALIGNED(row, 64)); 295 | } 296 | 297 | // Raw access to byte contents, for interfacing with other libraries. 298 | // Unsigned char instead of char to avoid surprises (sign extension). 299 | uint8_t * const BUTTERAUGLI_RESTRICT bytes() { return bytes_.get(); } 300 | const uint8_t * const BUTTERAUGLI_RESTRICT bytes() const { 301 | return bytes_.get(); 302 | } 303 | size_t bytes_per_row() const { return bytes_per_row_; } 304 | 305 | // Returns number of pixels (some of which are padding) per row. Useful for 306 | // computing other rows via pointer arithmetic. 307 | intptr_t PixelsPerRow() const { 308 | static_assert(CacheAligned::kCacheLineSize % sizeof(T) == 0, 309 | "Padding must be divisible by the pixel size."); 310 | return static_cast(bytes_per_row_ / sizeof(T)); 311 | } 312 | 313 | private: 314 | // Deleter used when bytes are not owned. 315 | static void Ignore(void *ptr) {} 316 | 317 | // (Members are non-const to enable assignment during move-assignment.) 318 | size_t xsize_; // original intended pixels, not including any padding. 319 | size_t ysize_; 320 | size_t bytes_per_row_; // [bytes] including padding. 321 | CacheAlignedUniquePtr bytes_; 322 | }; 323 | 324 | // Returns newly allocated planes of the given dimensions. 325 | template 326 | static inline std::vector> CreatePlanes(const size_t xsize, 327 | const size_t ysize, 328 | const size_t num_planes) { 329 | std::vector> planes; 330 | planes.reserve(num_planes); 331 | for (size_t i = 0; i < num_planes; ++i) { 332 | planes.emplace_back(xsize, ysize); 333 | } 334 | return planes; 335 | } 336 | 337 | // Returns a new image with the same dimensions and pixel values. 338 | template 339 | static inline Image CopyPixels(const Image &other) { 340 | Image copy(other.xsize(), other.ysize()); 341 | const void *BUTTERAUGLI_RESTRICT from = other.bytes(); 342 | void *BUTTERAUGLI_RESTRICT to = copy.bytes(); 343 | memcpy(to, from, other.ysize() * other.bytes_per_row()); 344 | return copy; 345 | } 346 | 347 | // Returns new planes with the same dimensions and pixel values. 348 | template 349 | static inline std::vector> CopyPlanes( 350 | const std::vector> &planes) { 351 | std::vector> copy; 352 | copy.reserve(planes.size()); 353 | for (const Image &plane : planes) { 354 | copy.push_back(CopyPixels(plane)); 355 | } 356 | return copy; 357 | } 358 | 359 | // Compacts a padded image into a preallocated packed vector. 360 | template 361 | static inline void CopyToPacked(const Image &from, std::vector *to) { 362 | const size_t xsize = from.xsize(); 363 | const size_t ysize = from.ysize(); 364 | #if BUTTERAUGLI_ENABLE_CHECKS 365 | if (to->size() < xsize * ysize) { 366 | printf("%zu x %zu exceeds %zu capacity\n", xsize, ysize, to->size()); 367 | abort(); 368 | } 369 | #endif 370 | for (size_t y = 0; y < ysize; ++y) { 371 | const float* const BUTTERAUGLI_RESTRICT row_from = from.Row(y); 372 | float* const BUTTERAUGLI_RESTRICT row_to = to->data() + y * xsize; 373 | memcpy(row_to, row_from, xsize * sizeof(T)); 374 | } 375 | } 376 | 377 | // Expands a packed vector into a preallocated padded image. 378 | template 379 | static inline void CopyFromPacked(const std::vector &from, Image *to) { 380 | const size_t xsize = to->xsize(); 381 | const size_t ysize = to->ysize(); 382 | assert(from.size() == xsize * ysize); 383 | for (size_t y = 0; y < ysize; ++y) { 384 | const float* const BUTTERAUGLI_RESTRICT row_from = 385 | from.data() + y * xsize; 386 | float* const BUTTERAUGLI_RESTRICT row_to = to->Row(y); 387 | memcpy(row_to, row_from, xsize * sizeof(T)); 388 | } 389 | } 390 | 391 | template 392 | static inline std::vector> PlanesFromPacked( 393 | const size_t xsize, const size_t ysize, 394 | const std::vector> &packed) { 395 | std::vector> planes; 396 | planes.reserve(packed.size()); 397 | for (const std::vector &p : packed) { 398 | planes.push_back(Image(xsize, ysize)); 399 | CopyFromPacked(p, &planes.back()); 400 | } 401 | return planes; 402 | } 403 | 404 | template 405 | static inline std::vector> PackedFromPlanes( 406 | const std::vector> &planes) { 407 | assert(!planes.empty()); 408 | const size_t num_pixels = planes[0].xsize() * planes[0].ysize(); 409 | std::vector> packed; 410 | packed.reserve(planes.size()); 411 | for (const Image &image : planes) { 412 | packed.push_back(std::vector(num_pixels)); 413 | CopyToPacked(image, &packed.back()); 414 | } 415 | return packed; 416 | } 417 | 418 | struct PsychoImage { 419 | std::vector uhf; 420 | std::vector hf; 421 | std::vector mf; 422 | std::vector lf; 423 | }; 424 | 425 | class ButteraugliComparator { 426 | public: 427 | ButteraugliComparator(const std::vector& rgb0, double hf_asymmetry); 428 | 429 | // Computes the butteraugli map between the original image given in the 430 | // constructor and the distorted image give here. 431 | void Diffmap(const std::vector& rgb1, ImageF& result) const; 432 | 433 | // Same as above, but OpsinDynamicsImage() was already applied. 434 | void DiffmapOpsinDynamicsImage(const std::vector& xyb1, 435 | ImageF& result) const; 436 | 437 | // Same as above, but the frequency decomposition was already applied. 438 | void DiffmapPsychoImage(const PsychoImage& ps1, ImageF &result) const; 439 | 440 | void Mask(std::vector* BUTTERAUGLI_RESTRICT mask, 441 | std::vector* BUTTERAUGLI_RESTRICT mask_dc) const; 442 | 443 | private: 444 | void MaltaDiffMapLF(const ImageF& y0, 445 | const ImageF& y1, 446 | double w_0gt1, 447 | double w_0lt1, 448 | double normalization, 449 | ImageF* BUTTERAUGLI_RESTRICT block_diff_ac) const; 450 | 451 | void MaltaDiffMap(const ImageF& y0, 452 | const ImageF& y1, 453 | double w_0gt1, 454 | double w_0lt1, 455 | double normalization, 456 | ImageF* BUTTERAUGLI_RESTRICT block_diff_ac) const; 457 | 458 | ImageF CombineChannels(const std::vector& scale_xyb, 459 | const std::vector& scale_xyb_dc, 460 | const std::vector& block_diff_dc, 461 | const std::vector& block_diff_ac) const; 462 | 463 | const size_t xsize_; 464 | const size_t ysize_; 465 | const size_t num_pixels_; 466 | float hf_asymmetry_; 467 | PsychoImage pi0_; 468 | }; 469 | 470 | void ButteraugliDiffmap(const std::vector &rgb0, 471 | const std::vector &rgb1, 472 | double hf_asymmetry, 473 | ImageF &diffmap); 474 | 475 | double ButteraugliScoreFromDiffmap(const ImageF& distmap); 476 | 477 | // Generate rgb-representation of the distance between two images. 478 | void CreateHeatMapImage(const std::vector &distmap, 479 | double good_threshold, double bad_threshold, 480 | size_t xsize, size_t ysize, 481 | std::vector *heatmap); 482 | 483 | // Compute values of local frequency and dc masking based on the activity 484 | // in the two images. 485 | void Mask(const std::vector& xyb0, 486 | const std::vector& xyb1, 487 | std::vector* BUTTERAUGLI_RESTRICT mask, 488 | std::vector* BUTTERAUGLI_RESTRICT mask_dc); 489 | 490 | template 491 | BUTTERAUGLI_INLINE void RgbToXyb(const V &r, const V &g, const V &b, 492 | V *BUTTERAUGLI_RESTRICT valx, 493 | V *BUTTERAUGLI_RESTRICT valy, 494 | V *BUTTERAUGLI_RESTRICT valb) { 495 | *valx = r - g; 496 | *valy = r + g; 497 | *valb = b; 498 | } 499 | 500 | template 501 | BUTTERAUGLI_INLINE void OpsinAbsorbance(const V &in0, const V &in1, 502 | const V &in2, 503 | V *BUTTERAUGLI_RESTRICT out0, 504 | V *BUTTERAUGLI_RESTRICT out1, 505 | V *BUTTERAUGLI_RESTRICT out2) { 506 | // https://en.wikipedia.org/wiki/Photopsin absorbance modeling. 507 | static const double mixi0 = 0.254462330846; 508 | static const double mixi1 = 0.488238255095; 509 | static const double mixi2 = 0.0635278003854; 510 | static const double mixi3 = 1.01681026909; 511 | static const double mixi4 = 0.195214015766; 512 | static const double mixi5 = 0.568019861857; 513 | static const double mixi6 = 0.0860755536007; 514 | static const double mixi7 = 1.1510118369; 515 | static const double mixi8 = 0.07374607900105684; 516 | static const double mixi9 = 0.06142425304154509; 517 | static const double mixi10 = 0.24416850520714256; 518 | static const double mixi11 = 1.20481945273; 519 | 520 | const V mix0(mixi0); 521 | const V mix1(mixi1); 522 | const V mix2(mixi2); 523 | const V mix3(mixi3); 524 | const V mix4(mixi4); 525 | const V mix5(mixi5); 526 | const V mix6(mixi6); 527 | const V mix7(mixi7); 528 | const V mix8(mixi8); 529 | const V mix9(mixi9); 530 | const V mix10(mixi10); 531 | const V mix11(mixi11); 532 | 533 | *out0 = mix0 * in0 + mix1 * in1 + mix2 * in2 + mix3; 534 | *out1 = mix4 * in0 + mix5 * in1 + mix6 * in2 + mix7; 535 | *out2 = mix8 * in0 + mix9 * in1 + mix10 * in2 + mix11; 536 | } 537 | 538 | std::vector OpsinDynamicsImage(const std::vector& rgb); 539 | 540 | ImageF Blur(const ImageF& in, float sigma, float border_ratio); 541 | 542 | double SimpleGamma(double v); 543 | 544 | double GammaMinArg(); 545 | double GammaMaxArg(); 546 | 547 | // Polynomial evaluation via Clenshaw's scheme (similar to Horner's). 548 | // Template enables compile-time unrolling of the recursion, but must reside 549 | // outside of a class due to the specialization. 550 | template 551 | static inline void ClenshawRecursion(const double x, const double *coefficients, 552 | double *b1, double *b2) { 553 | const double x_b1 = x * (*b1); 554 | const double t = (x_b1 + x_b1) - (*b2) + coefficients[INDEX]; 555 | *b2 = *b1; 556 | *b1 = t; 557 | 558 | ClenshawRecursion(x, coefficients, b1, b2); 559 | } 560 | 561 | // Base case 562 | template <> 563 | inline void ClenshawRecursion<0>(const double x, const double *coefficients, 564 | double *b1, double *b2) { 565 | const double x_b1 = x * (*b1); 566 | // The final iteration differs - no 2 * x_b1 here. 567 | *b1 = x_b1 - (*b2) + coefficients[0]; 568 | } 569 | 570 | // Rational polynomial := dividing two polynomial evaluations. These are easier 571 | // to find than minimax polynomials. 572 | struct RationalPolynomial { 573 | template 574 | static double EvaluatePolynomial(const double x, 575 | const double (&coefficients)[N]) { 576 | double b1 = 0.0; 577 | double b2 = 0.0; 578 | ClenshawRecursion(x, coefficients, &b1, &b2); 579 | return b1; 580 | } 581 | 582 | // Evaluates the polynomial at x (in [min_value, max_value]). 583 | inline double operator()(const double x) const { 584 | // First normalize to [0, 1]. 585 | const double x01 = (x - min_value) / (max_value - min_value); 586 | // And then to [-1, 1] domain of Chebyshev polynomials. 587 | const double xc = 2.0 * x01 - 1.0; 588 | 589 | const double yp = EvaluatePolynomial(xc, p); 590 | const double yq = EvaluatePolynomial(xc, q); 591 | if (yq == 0.0) return 0.0; 592 | return static_cast(yp / yq); 593 | } 594 | 595 | // Domain of the polynomials; they are undefined elsewhere. 596 | double min_value; 597 | double max_value; 598 | 599 | // Coefficients of T_n (Chebyshev polynomials of the first kind). 600 | // Degree 5/5 is a compromise between accuracy (0.1%) and numerical stability. 601 | double p[5 + 1]; 602 | double q[5 + 1]; 603 | }; 604 | 605 | static inline double GammaPolynomial(double value) { 606 | static const RationalPolynomial r = { 607 | 0.971783, 590.188894, 608 | { 609 | 98.7821300963361, 164.273222212631, 92.948112871376, 610 | 33.8165311212688, 6.91626704983562, 0.556380877028234 611 | }, 612 | { 613 | 1, 1.64339473427892, 0.89392405219969, 0.298947051776379, 614 | 0.0507146002577288, 0.00226495093949756 615 | }}; 616 | return r(value); 617 | } 618 | 619 | } // namespace butteraugli 620 | 621 | #endif // BUTTERAUGLI_BUTTERAUGLI_H_ 622 | -------------------------------------------------------------------------------- /butteraugli/butteraugli_main.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include "butteraugli/butteraugli.h" 6 | 7 | extern "C" { 8 | #include "png.h" 9 | #include "jpeglib.h" 10 | } 11 | 12 | namespace butteraugli { 13 | namespace { 14 | 15 | // "rgb": cleared and filled with same-sized image planes (one per channel); 16 | // either RGB, or RGBA if the PNG contains an alpha channel. 17 | bool ReadPNG(FILE* f, std::vector* rgb) { 18 | png_structp png_ptr = 19 | png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); 20 | if (!png_ptr) { 21 | return false; 22 | } 23 | 24 | png_infop info_ptr = png_create_info_struct(png_ptr); 25 | if (!info_ptr) { 26 | png_destroy_read_struct(&png_ptr, NULL, NULL); 27 | return false; 28 | } 29 | 30 | if (setjmp(png_jmpbuf(png_ptr)) != 0) { 31 | // Ok we are here because of the setjmp. 32 | png_destroy_read_struct(&png_ptr, &info_ptr, NULL); 33 | return false; 34 | } 35 | 36 | rewind(f); 37 | png_init_io(png_ptr, f); 38 | 39 | // The png_transforms flags are as follows: 40 | // packing == convert 1,2,4 bit images, 41 | // strip == 16 -> 8 bits / channel, 42 | // shift == use sBIT dynamics, and 43 | // expand == palettes -> rgb, grayscale -> 8 bit images, tRNS -> alpha. 44 | const unsigned int png_transforms = 45 | PNG_TRANSFORM_PACKING | PNG_TRANSFORM_EXPAND | PNG_TRANSFORM_STRIP_16; 46 | 47 | png_read_png(png_ptr, info_ptr, png_transforms, NULL); 48 | 49 | png_bytep* row_pointers = png_get_rows(png_ptr, info_ptr); 50 | 51 | const int xsize = png_get_image_width(png_ptr, info_ptr); 52 | const int ysize = png_get_image_height(png_ptr, info_ptr); 53 | const int components = png_get_channels(png_ptr, info_ptr); 54 | 55 | *rgb = CreatePlanes(xsize, ysize, 3); 56 | 57 | switch (components) { 58 | case 1: { 59 | // GRAYSCALE 60 | for (int y = 0; y < ysize; ++y) { 61 | const uint8_t* const BUTTERAUGLI_RESTRICT row = row_pointers[y]; 62 | uint8_t* const BUTTERAUGLI_RESTRICT row0 = (*rgb)[0].Row(y); 63 | uint8_t* const BUTTERAUGLI_RESTRICT row1 = (*rgb)[1].Row(y); 64 | uint8_t* const BUTTERAUGLI_RESTRICT row2 = (*rgb)[2].Row(y); 65 | 66 | for (int x = 0; x < xsize; ++x) { 67 | const uint8_t gray = row[x]; 68 | row0[x] = row1[x] = row2[x] = gray; 69 | } 70 | } 71 | break; 72 | } 73 | case 2: { 74 | // GRAYSCALE_ALPHA 75 | rgb->push_back(Image8(xsize, ysize)); 76 | for (int y = 0; y < ysize; ++y) { 77 | const uint8_t* const BUTTERAUGLI_RESTRICT row = row_pointers[y]; 78 | uint8_t* const BUTTERAUGLI_RESTRICT row0 = (*rgb)[0].Row(y); 79 | uint8_t* const BUTTERAUGLI_RESTRICT row1 = (*rgb)[1].Row(y); 80 | uint8_t* const BUTTERAUGLI_RESTRICT row2 = (*rgb)[2].Row(y); 81 | uint8_t* const BUTTERAUGLI_RESTRICT row3 = (*rgb)[3].Row(y); 82 | 83 | for (int x = 0; x < xsize; ++x) { 84 | const uint8_t gray = row[2 * x + 0]; 85 | const uint8_t alpha = row[2 * x + 1]; 86 | row0[x] = gray; 87 | row1[x] = gray; 88 | row2[x] = gray; 89 | row3[x] = alpha; 90 | } 91 | } 92 | break; 93 | } 94 | case 3: { 95 | // RGB 96 | for (int y = 0; y < ysize; ++y) { 97 | const uint8_t* const BUTTERAUGLI_RESTRICT row = row_pointers[y]; 98 | uint8_t* const BUTTERAUGLI_RESTRICT row0 = (*rgb)[0].Row(y); 99 | uint8_t* const BUTTERAUGLI_RESTRICT row1 = (*rgb)[1].Row(y); 100 | uint8_t* const BUTTERAUGLI_RESTRICT row2 = (*rgb)[2].Row(y); 101 | 102 | for (int x = 0; x < xsize; ++x) { 103 | row0[x] = row[3 * x + 0]; 104 | row1[x] = row[3 * x + 1]; 105 | row2[x] = row[3 * x + 2]; 106 | } 107 | } 108 | break; 109 | } 110 | case 4: { 111 | // RGBA 112 | rgb->push_back(Image8(xsize, ysize)); 113 | for (int y = 0; y < ysize; ++y) { 114 | const uint8_t* const BUTTERAUGLI_RESTRICT row = row_pointers[y]; 115 | uint8_t* const BUTTERAUGLI_RESTRICT row0 = (*rgb)[0].Row(y); 116 | uint8_t* const BUTTERAUGLI_RESTRICT row1 = (*rgb)[1].Row(y); 117 | uint8_t* const BUTTERAUGLI_RESTRICT row2 = (*rgb)[2].Row(y); 118 | uint8_t* const BUTTERAUGLI_RESTRICT row3 = (*rgb)[3].Row(y); 119 | 120 | for (int x = 0; x < xsize; ++x) { 121 | row0[x] = row[4 * x + 0]; 122 | row1[x] = row[4 * x + 1]; 123 | row2[x] = row[4 * x + 2]; 124 | row3[x] = row[4 * x + 3]; 125 | } 126 | } 127 | break; 128 | } 129 | default: 130 | png_destroy_read_struct(&png_ptr, &info_ptr, NULL); 131 | return false; 132 | } 133 | png_destroy_read_struct(&png_ptr, &info_ptr, NULL); 134 | return true; 135 | } 136 | 137 | const double* NewSrgbToLinearTable() { 138 | double* table = new double[256]; 139 | for (int i = 0; i < 256; ++i) { 140 | const double srgb = i / 255.0; 141 | table[i] = 142 | 255.0 * (srgb <= 0.04045 ? srgb / 12.92 143 | : std::pow((srgb + 0.055) / 1.055, 2.4)); 144 | } 145 | return table; 146 | } 147 | 148 | void jpeg_catch_error(j_common_ptr cinfo) { 149 | (*cinfo->err->output_message) (cinfo); 150 | jmp_buf* jpeg_jmpbuf = (jmp_buf*) cinfo->client_data; 151 | jpeg_destroy(cinfo); 152 | longjmp(*jpeg_jmpbuf, 1); 153 | } 154 | 155 | // "rgb": cleared and filled with same-sized image planes (one per channel); 156 | // either RGB, or RGBA if the PNG contains an alpha channel. 157 | bool ReadJPEG(FILE* f, std::vector* rgb) { 158 | rewind(f); 159 | 160 | struct jpeg_decompress_struct cinfo; 161 | struct jpeg_error_mgr jerr; 162 | cinfo.err = jpeg_std_error(&jerr); 163 | jmp_buf jpeg_jmpbuf; 164 | cinfo.client_data = &jpeg_jmpbuf; 165 | jerr.error_exit = jpeg_catch_error; 166 | if (setjmp(jpeg_jmpbuf)) { 167 | return false; 168 | } 169 | 170 | jpeg_create_decompress(&cinfo); 171 | 172 | jpeg_stdio_src(&cinfo, f); 173 | jpeg_read_header(&cinfo, TRUE); 174 | jpeg_start_decompress(&cinfo); 175 | 176 | int row_stride = cinfo.output_width * cinfo.output_components; 177 | JSAMPARRAY buffer = (*cinfo.mem->alloc_sarray) 178 | ((j_common_ptr) &cinfo, JPOOL_IMAGE, row_stride, 1); 179 | 180 | const size_t xsize = cinfo.output_width; 181 | const size_t ysize = cinfo.output_height; 182 | 183 | *rgb = CreatePlanes(xsize, ysize, 3); 184 | 185 | switch (cinfo.out_color_space) { 186 | case JCS_GRAYSCALE: 187 | while (cinfo.output_scanline < cinfo.output_height) { 188 | jpeg_read_scanlines(&cinfo, buffer, 1); 189 | 190 | const uint8_t* const BUTTERAUGLI_RESTRICT row = buffer[0]; 191 | uint8_t* const BUTTERAUGLI_RESTRICT row0 = 192 | (*rgb)[0].Row(cinfo.output_scanline - 1); 193 | uint8_t* const BUTTERAUGLI_RESTRICT row1 = 194 | (*rgb)[1].Row(cinfo.output_scanline - 1); 195 | uint8_t* const BUTTERAUGLI_RESTRICT row2 = 196 | (*rgb)[2].Row(cinfo.output_scanline - 1); 197 | 198 | for (int x = 0; x < xsize; x++) { 199 | const uint8_t gray = row[x]; 200 | row0[x] = row1[x] = row2[x] = gray; 201 | } 202 | } 203 | break; 204 | 205 | case JCS_RGB: 206 | while (cinfo.output_scanline < cinfo.output_height) { 207 | jpeg_read_scanlines(&cinfo, buffer, 1); 208 | 209 | const uint8_t* const BUTTERAUGLI_RESTRICT row = buffer[0]; 210 | uint8_t* const BUTTERAUGLI_RESTRICT row0 = 211 | (*rgb)[0].Row(cinfo.output_scanline - 1); 212 | uint8_t* const BUTTERAUGLI_RESTRICT row1 = 213 | (*rgb)[1].Row(cinfo.output_scanline - 1); 214 | uint8_t* const BUTTERAUGLI_RESTRICT row2 = 215 | (*rgb)[2].Row(cinfo.output_scanline - 1); 216 | 217 | for (int x = 0; x < xsize; x++) { 218 | row0[x] = row[3 * x + 0]; 219 | row1[x] = row[3 * x + 1]; 220 | row2[x] = row[3 * x + 2]; 221 | } 222 | } 223 | break; 224 | 225 | default: 226 | jpeg_destroy_decompress(&cinfo); 227 | return false; 228 | } 229 | 230 | jpeg_finish_decompress(&cinfo); 231 | jpeg_destroy_decompress(&cinfo); 232 | return true; 233 | } 234 | 235 | // Translate R, G, B channels from sRGB to linear space. If an alpha channel 236 | // is present, overlay the image over a black or white background. Overlaying 237 | // is done in the sRGB space; while technically incorrect, this is aligned with 238 | // many other software (web browsers, WebP near lossless). 239 | void FromSrgbToLinear(const std::vector& rgb, 240 | std::vector& linear, int background) { 241 | const size_t xsize = rgb[0].xsize(); 242 | const size_t ysize = rgb[0].ysize(); 243 | static const double* const kSrgbToLinearTable = NewSrgbToLinearTable(); 244 | 245 | if (rgb.size() == 3) { // RGB 246 | for (int c = 0; c < 3; c++) { 247 | linear.push_back(ImageF(xsize, ysize)); 248 | for (int y = 0; y < ysize; ++y) { 249 | const uint8_t* const BUTTERAUGLI_RESTRICT row_rgb = rgb[c].Row(y); 250 | float* const BUTTERAUGLI_RESTRICT row_linear = linear[c].Row(y); 251 | for (size_t x = 0; x < xsize; x++) { 252 | const int value = row_rgb[x]; 253 | row_linear[x] = kSrgbToLinearTable[value]; 254 | } 255 | } 256 | } 257 | } else { // RGBA 258 | for (int c = 0; c < 3; c++) { 259 | linear.push_back(ImageF(xsize, ysize)); 260 | for (int y = 0; y < ysize; ++y) { 261 | const uint8_t* const BUTTERAUGLI_RESTRICT row_rgb = rgb[c].Row(y); 262 | float* const BUTTERAUGLI_RESTRICT row_linear = linear[c].Row(y); 263 | const uint8_t* const BUTTERAUGLI_RESTRICT row_alpha = rgb[3].Row(y); 264 | for (size_t x = 0; x < xsize; x++) { 265 | int value; 266 | if (row_alpha[x] == 255) { 267 | value = row_rgb[x]; 268 | } else if (row_alpha[x] == 0) { 269 | value = background; 270 | } else { 271 | const int fg_weight = row_alpha[x]; 272 | const int bg_weight = 255 - fg_weight; 273 | value = 274 | (row_rgb[x] * fg_weight + background * bg_weight + 127) / 255; 275 | } 276 | row_linear[x] = kSrgbToLinearTable[value]; 277 | } 278 | } 279 | } 280 | } 281 | } 282 | 283 | std::vector ReadImageOrDie(const char* filename) { 284 | std::vector rgb; 285 | FILE* f = fopen(filename, "rb"); 286 | if (!f) { 287 | fprintf(stderr, "Cannot open %s\n", filename); 288 | exit(1); 289 | } 290 | unsigned char magic[2]; 291 | if (fread(magic, 1, 2, f) != 2) { 292 | fprintf(stderr, "Cannot read from %s\n", filename); 293 | exit(1); 294 | } 295 | if (magic[0] == 0xFF && magic[1] == 0xD8) { 296 | if (!ReadJPEG(f, &rgb)) { 297 | fprintf(stderr, "File %s is a malformed JPEG.\n", filename); 298 | exit(1); 299 | } 300 | } else { 301 | if (!ReadPNG(f, &rgb)) { 302 | fprintf(stderr, "File %s is neither a valid JPEG nor a valid PNG.\n", 303 | filename); 304 | exit(1); 305 | } 306 | } 307 | fclose(f); 308 | return rgb; 309 | } 310 | 311 | static void ScoreToRgb(double score, double good_threshold, 312 | double bad_threshold, uint8_t rgb[3]) { 313 | double heatmap[12][3] = { 314 | { 0, 0, 0 }, 315 | { 0, 0, 1 }, 316 | { 0, 1, 1 }, 317 | { 0, 1, 0 }, // Good level 318 | { 1, 1, 0 }, 319 | { 1, 0, 0 }, // Bad level 320 | { 1, 0, 1 }, 321 | { 0.5, 0.5, 1.0 }, 322 | { 1.0, 0.5, 0.5 }, // Pastel colors for the very bad quality range. 323 | { 1.0, 1.0, 0.5 }, 324 | { 1, 1, 1, }, 325 | { 1, 1, 1, }, 326 | }; 327 | if (score < good_threshold) { 328 | score = (score / good_threshold) * 0.3; 329 | } else if (score < bad_threshold) { 330 | score = 0.3 + (score - good_threshold) / 331 | (bad_threshold - good_threshold) * 0.15; 332 | } else { 333 | score = 0.45 + (score - bad_threshold) / 334 | (bad_threshold * 12) * 0.5; 335 | } 336 | static const int kTableSize = sizeof(heatmap) / sizeof(heatmap[0]); 337 | score = std::min(std::max( 338 | score * (kTableSize - 1), 0.0), kTableSize - 2); 339 | int ix = static_cast(score); 340 | double mix = score - ix; 341 | for (int i = 0; i < 3; ++i) { 342 | double v = mix * heatmap[ix + 1][i] + (1 - mix) * heatmap[ix][i]; 343 | rgb[i] = static_cast(255 * pow(v, 0.5) + 0.5); 344 | } 345 | } 346 | 347 | void CreateHeatMapImage(const ImageF& distmap, double good_threshold, 348 | double bad_threshold, size_t xsize, size_t ysize, 349 | std::vector* heatmap) { 350 | heatmap->resize(3 * xsize * ysize); 351 | for (size_t y = 0; y < ysize; ++y) { 352 | for (size_t x = 0; x < xsize; ++x) { 353 | int px = xsize * y + x; 354 | double d = distmap.Row(y)[x]; 355 | uint8_t* rgb = &(*heatmap)[3 * px]; 356 | ScoreToRgb(d, good_threshold, bad_threshold, rgb); 357 | } 358 | } 359 | } 360 | 361 | // main() function, within butteraugli namespace for convenience. 362 | int Run(int argc, char* argv[]) { 363 | if (argc != 3 && argc != 4) { 364 | fprintf(stderr, 365 | "Usage: %s {image1.(png|jpg|jpeg)} {image2.(png|jpg|jpeg)} " 366 | "[heatmap.ppm]\n", 367 | argv[0]); 368 | return 1; 369 | } 370 | 371 | std::vector rgb1 = ReadImageOrDie(argv[1]); 372 | std::vector rgb2 = ReadImageOrDie(argv[2]); 373 | 374 | if (rgb1.size() == 3 && rgb2.size() == 4) { 375 | // Adding a missing alpha channel to one of the images. 376 | rgb1.push_back(Image8(rgb1[0].xsize(), rgb1[0].ysize(), 255)); 377 | } else if (rgb2.size() == 3 && rgb1.size() == 4) { 378 | // Adding a missing alpha channel to one of the images. 379 | rgb2.push_back(Image8(rgb2[0].xsize(), rgb2[0].ysize(), 255)); 380 | } else if (rgb1.size() != rgb2.size()) { 381 | fprintf(stderr, "Different number of channels: %lu vs %lu\n", rgb1.size(), 382 | rgb2.size()); 383 | exit(1); 384 | } 385 | 386 | for (size_t c = 0; c < rgb1.size(); ++c) { 387 | if (rgb1[c].xsize() != rgb2[c].xsize() || 388 | rgb1[c].ysize() != rgb2[c].ysize()) { 389 | fprintf( 390 | stderr, "The images are not equal in size: (%lu,%lu) vs (%lu,%lu)\n", 391 | rgb1[c].xsize(), rgb2[c].xsize(), rgb1[c].ysize(), rgb2[c].ysize()); 392 | return 1; 393 | } 394 | } 395 | 396 | // TODO: Figure out if it is a good idea to fetch the gamma from the image 397 | // instead of applying sRGB conversion. 398 | std::vector linear1, linear2; 399 | // Overlay the image over a black background. 400 | FromSrgbToLinear(rgb1, linear1, 0); 401 | FromSrgbToLinear(rgb2, linear2, 0); 402 | ImageF diff_map, diff_map_on_white; 403 | double diff_value; 404 | if (!butteraugli::ButteraugliInterface(linear1, linear2, 1.0, 405 | diff_map, diff_value)) { 406 | fprintf(stderr, "Butteraugli comparison failed\n"); 407 | return 1; 408 | } 409 | ImageF* diff_map_ptr = &diff_map; 410 | if (rgb1.size() == 4 || rgb2.size() == 4) { 411 | // If the alpha channel is present, overlay the image over a white 412 | // background as well. 413 | FromSrgbToLinear(rgb1, linear1, 255); 414 | FromSrgbToLinear(rgb2, linear2, 255); 415 | double diff_value_on_white; 416 | if (!butteraugli::ButteraugliInterface(linear1, linear2, 1.0, 417 | diff_map_on_white, 418 | diff_value_on_white)) { 419 | fprintf(stderr, "Butteraugli comparison failed\n"); 420 | return 1; 421 | } 422 | if (diff_value_on_white > diff_value) { 423 | diff_value = diff_value_on_white; 424 | diff_map_ptr = &diff_map_on_white; 425 | } 426 | } 427 | printf("%lf\n", diff_value); 428 | 429 | if (argc == 4) { 430 | const double good_quality = ::butteraugli::ButteraugliFuzzyInverse(1.5); 431 | const double bad_quality = ::butteraugli::ButteraugliFuzzyInverse(0.5); 432 | std::vector rgb; 433 | CreateHeatMapImage(*diff_map_ptr, good_quality, bad_quality, 434 | rgb1[0].xsize(), rgb2[0].ysize(), &rgb); 435 | FILE* const fmap = fopen(argv[3], "wb"); 436 | if (fmap == NULL) { 437 | fprintf(stderr, "Cannot open %s\n", argv[3]); 438 | perror("fopen"); 439 | return 1; 440 | } 441 | bool ok = true; 442 | if (fprintf(fmap, "P6\n%lu %lu\n255\n", 443 | rgb1[0].xsize(), rgb1[0].ysize()) < 0){ 444 | perror("fprintf"); 445 | ok = false; 446 | } 447 | if (ok && fwrite(rgb.data(), 1, rgb.size(), fmap) != rgb.size()) { 448 | perror("fwrite"); 449 | ok = false; 450 | } 451 | if (fclose(fmap) != 0) { 452 | perror("fclose"); 453 | ok = false; 454 | } 455 | if (!ok) return 1; 456 | } 457 | 458 | return 0; 459 | } 460 | 461 | } // namespace 462 | } // namespace butteraugli 463 | 464 | int main(int argc, char** argv) { return butteraugli::Run(argc, argv); } 465 | -------------------------------------------------------------------------------- /docs/comparison.html: -------------------------------------------------------------------------------- 1 | Butteraugli comparisons

Butteraugli comparisons

This page compares Butteraugli against a number of image difference metrics. It shows examples of images where they disagree, to gain insight into what kind of differences those metrics are sensitive to.

Image pairs where Butteraugli and the other metric disagree the most are shown in mosaics, and the corresponding points are highlighted in the plots. The left half of the mosaic shows the green points in the plot, the points that Butteraugli ranks among the least different, but which the other metric ranks among the most different. The right half of the mosaic shows the orange points in the plot, which Butteraugli ranks among the most different, but which the other metric ranks among the least different.

Click “Toggle A/B” to flip between the images in a pair. Zooming in with Ctrl + Scrollwheel can help to see subtle differences.

Butteraugli versus SSIM


Mosaic of Butteraugli versus SSIMScatter plot of Butteraugli versus SSIM

Butteraugli versus PSNR


Mosaic of Butteraugli versus PSNRScatter plot of Butteraugli versus PSNR

Butteraugli versus PSNRHVS-M


Mosaic of Butteraugli versus PSNRHVS-MScatter plot of Butteraugli versus PSNRHVS-M

Butteraugli versus MAE


Mosaic of Butteraugli versus MAEScatter plot of Butteraugli versus MAE

Butteraugli versus FUZZ


Mosaic of Butteraugli versus FUZZScatter plot of Butteraugli versus FUZZ

Butteraugli versus NCC


Mosaic of Butteraugli versus NCCScatter plot of Butteraugli versus NCC

Butteraugli versus SSIMULACRA


Mosaic of Butteraugli versus SSIMULACRAScatter plot of Butteraugli versus SSIMULACRA
2 | -------------------------------------------------------------------------------- /docs/vs_fuzz_butteraugli_heatmap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/butteraugli/71b18b636b9c7d1ae0c1d3730b85b3c127eb4511/docs/vs_fuzz_butteraugli_heatmap.png -------------------------------------------------------------------------------- /docs/vs_fuzz_left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/butteraugli/71b18b636b9c7d1ae0c1d3730b85b3c127eb4511/docs/vs_fuzz_left.png -------------------------------------------------------------------------------- /docs/vs_fuzz_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/butteraugli/71b18b636b9c7d1ae0c1d3730b85b3c127eb4511/docs/vs_fuzz_right.png -------------------------------------------------------------------------------- /docs/vs_mae_butteraugli_heatmap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/butteraugli/71b18b636b9c7d1ae0c1d3730b85b3c127eb4511/docs/vs_mae_butteraugli_heatmap.png -------------------------------------------------------------------------------- /docs/vs_mae_left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/butteraugli/71b18b636b9c7d1ae0c1d3730b85b3c127eb4511/docs/vs_mae_left.png -------------------------------------------------------------------------------- /docs/vs_mae_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/butteraugli/71b18b636b9c7d1ae0c1d3730b85b3c127eb4511/docs/vs_mae_right.png -------------------------------------------------------------------------------- /docs/vs_ncc_butteraugli_heatmap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/butteraugli/71b18b636b9c7d1ae0c1d3730b85b3c127eb4511/docs/vs_ncc_butteraugli_heatmap.png -------------------------------------------------------------------------------- /docs/vs_ncc_left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/butteraugli/71b18b636b9c7d1ae0c1d3730b85b3c127eb4511/docs/vs_ncc_left.png -------------------------------------------------------------------------------- /docs/vs_ncc_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/butteraugli/71b18b636b9c7d1ae0c1d3730b85b3c127eb4511/docs/vs_ncc_right.png -------------------------------------------------------------------------------- /docs/vs_psnr_butteraugli_heatmap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/butteraugli/71b18b636b9c7d1ae0c1d3730b85b3c127eb4511/docs/vs_psnr_butteraugli_heatmap.png -------------------------------------------------------------------------------- /docs/vs_psnr_left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/butteraugli/71b18b636b9c7d1ae0c1d3730b85b3c127eb4511/docs/vs_psnr_left.png -------------------------------------------------------------------------------- /docs/vs_psnr_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/butteraugli/71b18b636b9c7d1ae0c1d3730b85b3c127eb4511/docs/vs_psnr_right.png -------------------------------------------------------------------------------- /docs/vs_psnrhvs_m_butteraugli_heatmap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/butteraugli/71b18b636b9c7d1ae0c1d3730b85b3c127eb4511/docs/vs_psnrhvs_m_butteraugli_heatmap.png -------------------------------------------------------------------------------- /docs/vs_psnrhvs_m_left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/butteraugli/71b18b636b9c7d1ae0c1d3730b85b3c127eb4511/docs/vs_psnrhvs_m_left.png -------------------------------------------------------------------------------- /docs/vs_psnrhvs_m_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/butteraugli/71b18b636b9c7d1ae0c1d3730b85b3c127eb4511/docs/vs_psnrhvs_m_right.png -------------------------------------------------------------------------------- /docs/vs_ssim_butteraugli_heatmap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/butteraugli/71b18b636b9c7d1ae0c1d3730b85b3c127eb4511/docs/vs_ssim_butteraugli_heatmap.png -------------------------------------------------------------------------------- /docs/vs_ssim_left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/butteraugli/71b18b636b9c7d1ae0c1d3730b85b3c127eb4511/docs/vs_ssim_left.png -------------------------------------------------------------------------------- /docs/vs_ssim_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/butteraugli/71b18b636b9c7d1ae0c1d3730b85b3c127eb4511/docs/vs_ssim_right.png -------------------------------------------------------------------------------- /docs/vs_ssimulacra_butteraugli_heatmap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/butteraugli/71b18b636b9c7d1ae0c1d3730b85b3c127eb4511/docs/vs_ssimulacra_butteraugli_heatmap.png -------------------------------------------------------------------------------- /docs/vs_ssimulacra_left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/butteraugli/71b18b636b9c7d1ae0c1d3730b85b3c127eb4511/docs/vs_ssimulacra_left.png -------------------------------------------------------------------------------- /docs/vs_ssimulacra_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/butteraugli/71b18b636b9c7d1ae0c1d3730b85b3c127eb4511/docs/vs_ssimulacra_right.png -------------------------------------------------------------------------------- /jpeg.BUILD: -------------------------------------------------------------------------------- 1 | # Description: 2 | # The Independent JPEG Group's JPEG runtime library. 3 | 4 | licenses(["notice"]) # custom notice-style license, see LICENSE 5 | 6 | cc_library( 7 | name = "jpeg", 8 | srcs = [ 9 | "cderror.h", 10 | "cdjpeg.h", 11 | "jaricom.c", 12 | "jcapimin.c", 13 | "jcapistd.c", 14 | "jcarith.c", 15 | "jccoefct.c", 16 | "jccolor.c", 17 | "jcdctmgr.c", 18 | "jchuff.c", 19 | "jcinit.c", 20 | "jcmainct.c", 21 | "jcmarker.c", 22 | "jcmaster.c", 23 | "jcomapi.c", 24 | "jconfig.h", 25 | "jcparam.c", 26 | "jcprepct.c", 27 | "jcsample.c", 28 | "jctrans.c", 29 | "jdapimin.c", 30 | "jdapistd.c", 31 | "jdarith.c", 32 | "jdatadst.c", 33 | "jdatasrc.c", 34 | "jdcoefct.c", 35 | "jdcolor.c", 36 | "jdct.h", 37 | "jddctmgr.c", 38 | "jdhuff.c", 39 | "jdinput.c", 40 | "jdmainct.c", 41 | "jdmarker.c", 42 | "jdmaster.c", 43 | "jdmerge.c", 44 | "jdpostct.c", 45 | "jdsample.c", 46 | "jdtrans.c", 47 | "jerror.c", 48 | "jfdctflt.c", 49 | "jfdctfst.c", 50 | "jfdctint.c", 51 | "jidctflt.c", 52 | "jidctfst.c", 53 | "jidctint.c", 54 | "jinclude.h", 55 | "jmemmgr.c", 56 | "jmemnobs.c", 57 | "jmemsys.h", 58 | "jmorecfg.h", 59 | "jquant1.c", 60 | "jquant2.c", 61 | "jutils.c", 62 | "jversion.h", 63 | "transupp.h", 64 | ], 65 | hdrs = [ 66 | "jerror.h", 67 | "jpegint.h", 68 | "jpeglib.h", 69 | ], 70 | includes = ["."], 71 | visibility = ["//visibility:public"], 72 | ) 73 | 74 | genrule( 75 | name = "configure", 76 | outs = ["jconfig.h"], 77 | cmd = "cat <$@\n" + 78 | "#define HAVE_PROTOTYPES 1\n" + 79 | "#define HAVE_UNSIGNED_CHAR 1\n" + 80 | "#define HAVE_UNSIGNED_SHORT 1\n" + 81 | "#define HAVE_STDDEF_H 1\n" + 82 | "#define HAVE_STDLIB_H 1\n" + 83 | "#ifdef WIN32\n" + 84 | "#define INLINE __inline\n" + 85 | "#else\n" + 86 | "#define INLINE __inline__\n" + 87 | "#endif\n" + 88 | "EOF\n", 89 | ) 90 | -------------------------------------------------------------------------------- /png.BUILD: -------------------------------------------------------------------------------- 1 | # Description: 2 | # libpng is the official PNG reference library. 3 | 4 | licenses(["notice"]) # BSD/MIT-like license 5 | 6 | cc_library( 7 | name = "png", 8 | srcs = [ 9 | "png.c", 10 | "pngerror.c", 11 | "pngget.c", 12 | "pngmem.c", 13 | "pngpread.c", 14 | "pngread.c", 15 | "pngrio.c", 16 | "pngrtran.c", 17 | "pngrutil.c", 18 | "pngset.c", 19 | "pngtrans.c", 20 | "pngwio.c", 21 | "pngwrite.c", 22 | "pngwtran.c", 23 | "pngwutil.c", 24 | ], 25 | hdrs = [ 26 | "png.h", 27 | "pngconf.h", 28 | ], 29 | includes = ["."], 30 | linkopts = ["-lm"], 31 | visibility = ["//visibility:public"], 32 | deps = ["@zlib_archive//:zlib"], 33 | ) 34 | -------------------------------------------------------------------------------- /zlib.BUILD: -------------------------------------------------------------------------------- 1 | package(default_visibility = ["//visibility:public"]) 2 | 3 | licenses(["notice"]) # BSD/MIT-like license (for zlib) 4 | 5 | cc_library( 6 | name = "zlib", 7 | srcs = [ 8 | "adler32.c", 9 | "compress.c", 10 | "crc32.c", 11 | "crc32.h", 12 | "deflate.c", 13 | "deflate.h", 14 | "gzclose.c", 15 | "gzguts.h", 16 | "gzlib.c", 17 | "gzread.c", 18 | "gzwrite.c", 19 | "infback.c", 20 | "inffast.c", 21 | "inffast.h", 22 | "inffixed.h", 23 | "inflate.c", 24 | "inflate.h", 25 | "inftrees.c", 26 | "inftrees.h", 27 | "trees.c", 28 | "trees.h", 29 | "uncompr.c", 30 | "zconf.h", 31 | "zutil.c", 32 | "zutil.h", 33 | ], 34 | hdrs = ["zlib.h"], 35 | includes = ["."], 36 | ) 37 | --------------------------------------------------------------------------------