// Copyright 2016 Google Inc. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // Author: Jyrki Alakuijala (jyrki.alakuijala@gmail.com) // // The physical architecture of butteraugli is based on the following naming // convention: // * Opsin - dynamics of the photosensitive chemicals in the retina // with their immediate electrical processing // * Xyb - hybrid opponent/trichromatic color space // x is roughly red-subtract-green. // y is yellow. // b is blue. // Xyb values are computed from Opsin mixing, not directly from rgb. // * Mask - for visual masking // * Hf - color modeling for spatially high-frequency features // * Lf - color modeling for spatially low-frequency features // * Diffmap - to cluster and build an image of error between the images // * Blur - to hold the smoothing code #include "butteraugli.h" #include #include #include #include #include #include #include // Restricted pointers speed up Convolution(); MSVC uses a different keyword. #ifdef _MSC_VER #define __restrict__ __restrict #endif namespace butteraugli { void *CacheAligned::Allocate(const size_t bytes) { char *const allocated = static_cast(malloc(bytes + kCacheLineSize)); if (allocated == nullptr) { return nullptr; } const uintptr_t misalignment = reinterpret_cast(allocated) & (kCacheLineSize - 1); // malloc is at least kPointerSize aligned, so we can store the "allocated" // pointer immediately before the aligned memory. assert(misalignment % kPointerSize == 0); char *const aligned = allocated + kCacheLineSize - misalignment; memcpy(aligned - kPointerSize, &allocated, kPointerSize); return aligned; } void CacheAligned::Free(void *aligned_pointer) { if (aligned_pointer == nullptr) { return; } char *const aligned = static_cast(aligned_pointer); assert(reinterpret_cast(aligned) % kCacheLineSize == 0); char *allocated; memcpy(&allocated, aligned - kPointerSize, kPointerSize); assert(allocated <= aligned - kPointerSize); assert(allocated >= aligned - kCacheLineSize); free(allocated); } static inline bool IsNan(const float x) { uint32_t bits; memcpy(&bits, &x, sizeof(bits)); const uint32_t bitmask_exp = 0x7F800000; return (bits & bitmask_exp) == bitmask_exp && (bits & 0x7FFFFF); } static inline bool IsNan(const double x) { uint64_t bits; memcpy(&bits, &x, sizeof(bits)); return (0x7ff0000000000001ULL <= bits && bits <= 0x7fffffffffffffffULL) || (0xfff0000000000001ULL <= bits && bits <= 0xffffffffffffffffULL); } static inline void CheckImage(const ImageF &image, const char *name) { for (size_t y = 0; y < image.ysize(); ++y) { ConstRestrict row = image.Row(y); for (size_t x = 0; x < image.xsize(); ++x) { if (IsNan(row[x])) { printf("Image %s @ %lu,%lu (of %lu,%lu)\n", name, x, y, image.xsize(), image.ysize()); exit(1); } } } } #if BUTTERAUGLI_ENABLE_CHECKS #define CHECK_NAN(x, str) \ do { \ if (IsNan(x)) { \ printf("%d: %s\n", __LINE__, str); \ abort(); \ } \ } while (0) #define CHECK_IMAGE(image, name) CheckImage(image, name) #else #define CHECK_NAN(x, str) #define CHECK_IMAGE(image, name) #endif static const double kInternalGoodQualityThreshold = 14.921561160295326; static const double kGlobalScale = 1.0 / kInternalGoodQualityThreshold; inline double DotProduct(const double u[3], const double v[3]) { return u[0] * v[0] + u[1] * v[1] + u[2] * v[2]; } inline double DotProduct(const float u[3], const double v[3]) { return u[0] * v[0] + u[1] * v[1] + u[2] * v[2]; } // Computes a horizontal convolution and transposes the result. static void Convolution(size_t xsize, size_t ysize, size_t xstep, size_t len, size_t offset, const float* __restrict__ multipliers, const float* __restrict__ inp, double border_ratio, float* __restrict__ result) { PROFILER_FUNC; double weight_no_border = 0; for (int j = 0; j <= 2 * offset; ++j) { weight_no_border += multipliers[j]; } for (size_t x = 0, ox = 0; x < xsize; x += xstep, ox++) { int minx = x < offset ? 0 : x - offset; int maxx = std::min(xsize, x + len - offset) - 1; double weight = 0.0; for (int j = minx; j <= maxx; ++j) { weight += multipliers[j - x + offset]; } // Interpolate linearly between the no-border scaling and border scaling. weight = (1.0 - border_ratio) * weight + border_ratio * weight_no_border; double scale = 1.0 / weight; for (size_t y = 0; y < ysize; ++y) { double sum = 0.0; for (int j = minx; j <= maxx; ++j) { sum += inp[y * xsize + j] * multipliers[j - x + offset]; } result[ox * ysize + y] = sum * scale; } } } void Blur(size_t xsize, size_t ysize, float* channel, double sigma, double border_ratio) { PROFILER_FUNC; double m = 2.25; // Accuracy increases when m is increased. const double scaler = -1.0 / (2 * sigma * sigma); // For m = 9.0: exp(-scaler * diff * diff) < 2^ {-52} const int diff = std::max(1, m * fabs(sigma)); const int expn_size = 2 * diff + 1; std::vector expn(expn_size); for (int i = -diff; i <= diff; ++i) { expn[i + diff] = exp(scaler * i * i); } const int xstep = std::max(1, int(sigma / 3)); const int ystep = xstep; int dxsize = (xsize + xstep - 1) / xstep; int dysize = (ysize + ystep - 1) / ystep; std::vector tmp(dxsize * ysize); std::vector downsampled_output(dxsize * dysize); Convolution(xsize, ysize, xstep, expn_size, diff, expn.data(), channel, border_ratio, tmp.data()); Convolution(ysize, dxsize, ystep, expn_size, diff, expn.data(), tmp.data(), border_ratio, downsampled_output.data()); for (int y = 0; y < ysize; y++) { for (int x = 0; x < xsize; x++) { // TODO: Use correct rounding. channel[y * xsize + x] = downsampled_output[(y / ystep) * dxsize + (x / xstep)]; } } } // To change this to n, add the relevant FFTn function and kFFTnMapIndexTable. constexpr size_t kBlockEdge = 8; constexpr size_t kBlockSize = kBlockEdge * kBlockEdge; constexpr size_t kBlockEdgeHalf = kBlockEdge / 2; constexpr size_t kBlockHalf = kBlockEdge * kBlockEdgeHalf; // Contrast sensitivity related weights. static const double *GetContrastSensitivityMatrix() { static double csf8x8[kBlockHalf + kBlockEdgeHalf + 1] = { 5.28270670524, 0.0, 0.0, 0.0, 0.3831134973, 0.676303603859, 3.58927792424, 18.6104367002, 18.6104367002, 3.09093131948, 1.0, 0.498250875965, 0.36198671102, 0.308982169883, 0.1312701920435, 2.37370549629, 3.58927792424, 1.0, 2.37370549629, 0.991205724152, 1.05178802919, 0.627264168628, 0.4, 0.1312701920435, 0.676303603859, 0.498250875965, 0.991205724152, 0.5, 0.3831134973, 0.349686450518, 0.627264168628, 0.308982169883, 0.3831134973, 0.36198671102, 1.05178802919, 0.3831134973, 0.12, }; return &csf8x8[0]; } std::array MakeHighFreqColorDiffDx() { std::array lut; static const double off = 11.38708334481672; static const double inc = 14.550189611520716; lut[1] = off; for (int i = 2; i < 21; ++i) { lut[i] = lut[i - 1] + inc; } return lut; } const double *GetHighFreqColorDiffDx() { static const std::array kLut = MakeHighFreqColorDiffDx(); return kLut.data(); } std::array MakeHighFreqColorDiffDy() { std::array lut; static const double off = 1.4103373714040413; static const double inc = 0.7084088867024; lut[1] = off; for (int i = 2; i < 21; ++i) { lut[i] = lut[i - 1] + inc; } return lut; } const double *GetHighFreqColorDiffDy() { static const std::array kLut = MakeHighFreqColorDiffDy(); return kLut.data(); } std::array MakeLowFreqColorDiffDy() { std::array lut; static const double inc = 5.2511644570349185; for (int i = 1; i < 21; ++i) { lut[i] = lut[i - 1] + inc; } return lut; } const double *GetLowFreqColorDiffDy() { static const std::array kLut = MakeLowFreqColorDiffDy(); return kLut.data(); } inline double Interpolate(const double *array, int size, double sx) { double ix = fabs(sx); assert(ix < 10000); int baseix = static_cast(ix); double res; if (baseix >= size - 1) { res = array[size - 1]; } else { double mix = ix - baseix; int nextix = baseix + 1; res = array[baseix] + mix * (array[nextix] - array[baseix]); } if (sx < 0) res = -res; return res; } inline double InterpolateClampNegative(const double *array, int size, double sx) { if (sx < 0) { sx = 0; } double ix = fabs(sx); int baseix = static_cast(ix); double res; if (baseix >= size - 1) { res = array[size - 1]; } else { double mix = ix - baseix; int nextix = baseix + 1; res = array[baseix] + mix * (array[nextix] - array[baseix]); } return res; } void RgbToXyb(double r, double g, double b, double *valx, double *valy, double *valz) { static const double a0 = 1.01611726948; static const double a1 = 0.982482243696; static const double a2 = 1.43571362627; static const double a3 = 0.896039849412; *valx = a0 * r - a1 * g; *valy = a2 * r + a3 * g; *valz = b; } static inline void XybToVals(double x, double y, double z, double *valx, double *valy, double *valz) { static const double xmul = 0.758304045695; static const double ymul = 2.28148649801; static const double zmul = 1.87816926918; *valx = Interpolate(GetHighFreqColorDiffDx(), 21, x * xmul); *valy = Interpolate(GetHighFreqColorDiffDy(), 21, y * ymul); *valz = zmul * z; } // Rough psychovisual distance to gray for low frequency colors. static void XybLowFreqToVals(double x, double y, double z, double *valx, double *valy, double *valz) { static const double xmul = 6.64482198135; static const double ymul = 0.837846224276; static const double zmul = 7.34905756986; static const double y_to_z_mul = 0.0812519812628; z += y_to_z_mul * y; *valz = z * zmul; *valx = x * xmul; *valy = Interpolate(GetLowFreqColorDiffDy(), 21, y * ymul); } double RemoveRangeAroundZero(double v, double range) { if (v >= -range && v < range) { return 0; } if (v < 0) { return v + range; } else { return v - range; } } void XybDiffLowFreqSquaredAccumulate(double r0, double g0, double b0, double r1, double g1, double b1, double factor, double res[3]) { double valx0, valy0, valz0; double valx1, valy1, valz1; XybLowFreqToVals(r0, g0, b0, &valx0, &valy0, &valz0); if (r1 == 0.0 && g1 == 0.0 && b1 == 0.0) { PROFILER_ZONE("XybDiff r1=g1=b1=0"); res[0] += factor * valx0 * valx0; res[1] += factor * valy0 * valy0; res[2] += factor * valz0 * valz0; return; } XybLowFreqToVals(r1, g1, b1, &valx1, &valy1, &valz1); // Approximate the distance of the colors by their respective distances // to gray. double valx = valx0 - valx1; double valy = valy0 - valy1; double valz = valz0 - valz1; res[0] += factor * valx * valx; res[1] += factor * valy * valy; res[2] += factor * valz * valz; } struct Complex { public: double real; double imag; }; inline double abssq(const Complex& c) { return c.real * c.real + c.imag * c.imag; } static void TransposeBlock(Complex data[kBlockSize]) { for (int i = 0; i < kBlockEdge; i++) { for (int j = 0; j < i; j++) { std::swap(data[kBlockEdge * i + j], data[kBlockEdge * j + i]); } } } // D. J. Bernstein's Fast Fourier Transform algorithm on 4 elements. inline void FFT4(Complex* a) { double t1, t2, t3, t4, t5, t6, t7, t8; t5 = a[2].real; t1 = a[0].real - t5; t7 = a[3].real; t5 += a[0].real; t3 = a[1].real - t7; t7 += a[1].real; t8 = t5 + t7; a[0].real = t8; t5 -= t7; a[1].real = t5; t6 = a[2].imag; t2 = a[0].imag - t6; t6 += a[0].imag; t5 = a[3].imag; a[2].imag = t2 + t3; t2 -= t3; a[3].imag = t2; t4 = a[1].imag - t5; a[3].real = t1 + t4; t1 -= t4; a[2].real = t1; t5 += a[1].imag; a[0].imag = t6 + t5; t6 -= t5; a[1].imag = t6; } static const double kSqrtHalf = 0.70710678118654752440084436210484903; // D. J. Bernstein's Fast Fourier Transform algorithm on 8 elements. void FFT8(Complex* a) { double t1, t2, t3, t4, t5, t6, t7, t8; t7 = a[4].imag; t4 = a[0].imag - t7; t7 += a[0].imag; a[0].imag = t7; t8 = a[6].real; t5 = a[2].real - t8; t8 += a[2].real; a[2].real = t8; t7 = a[6].imag; a[6].imag = t4 - t5; t4 += t5; a[4].imag = t4; t6 = a[2].imag - t7; t7 += a[2].imag; a[2].imag = t7; t8 = a[4].real; t3 = a[0].real - t8; t8 += a[0].real; a[0].real = t8; a[4].real = t3 - t6; t3 += t6; a[6].real = t3; t7 = a[5].real; t3 = a[1].real - t7; t7 += a[1].real; a[1].real = t7; t8 = a[7].imag; t6 = a[3].imag - t8; t8 += a[3].imag; a[3].imag = t8; t1 = t3 - t6; t3 += t6; t7 = a[5].imag; t4 = a[1].imag - t7; t7 += a[1].imag; a[1].imag = t7; t8 = a[7].real; t5 = a[3].real - t8; t8 += a[3].real; a[3].real = t8; t2 = t4 - t5; t4 += t5; t6 = t1 - t4; t8 = kSqrtHalf; t6 *= t8; a[5].real = a[4].real - t6; t1 += t4; t1 *= t8; a[5].imag = a[4].imag - t1; t6 += a[4].real; a[4].real = t6; t1 += a[4].imag; a[4].imag = t1; t5 = t2 - t3; t5 *= t8; a[7].imag = a[6].imag - t5; t2 += t3; t2 *= t8; a[7].real = a[6].real - t2; t2 += a[6].real; a[6].real = t2; t5 += a[6].imag; a[6].imag = t5; FFT4(a); // Reorder to the correct output order. // TODO: Modify the above computation so that this is not needed. Complex tmp = a[2]; a[2] = a[3]; a[3] = a[5]; a[5] = a[7]; a[7] = a[4]; a[4] = a[1]; a[1] = a[6]; a[6] = tmp; } // Same as FFT8, but all inputs are real. // TODO: Since this does not need to be in-place, maybe there is a // faster FFT than this one, which is derived from DJB's in-place complex FFT. void RealFFT8(const double* in, Complex* out) { double t1, t2, t3, t5, t6, t7, t8; t8 = in[6]; t5 = in[2] - t8; t8 += in[2]; out[2].real = t8; out[6].imag = -t5; out[4].imag = t5; t8 = in[4]; t3 = in[0] - t8; t8 += in[0]; out[0].real = t8; out[4].real = t3; out[6].real = t3; t7 = in[5]; t3 = in[1] - t7; t7 += in[1]; out[1].real = t7; t8 = in[7]; t5 = in[3] - t8; t8 += in[3]; out[3].real = t8; t2 = -t5; t6 = t3 - t5; t8 = kSqrtHalf; t6 *= t8; out[5].real = out[4].real - t6; t1 = t3 + t5; t1 *= t8; out[5].imag = out[4].imag - t1; t6 += out[4].real; out[4].real = t6; t1 += out[4].imag; out[4].imag = t1; t5 = t2 - t3; t5 *= t8; out[7].imag = out[6].imag - t5; t2 += t3; t2 *= t8; out[7].real = out[6].real - t2; t2 += out[6].real; out[6].real = t2; t5 += out[6].imag; out[6].imag = t5; t5 = out[2].real; t1 = out[0].real - t5; t7 = out[3].real; t5 += out[0].real; t3 = out[1].real - t7; t7 += out[1].real; t8 = t5 + t7; out[0].real = t8; t5 -= t7; out[1].real = t5; out[2].imag = t3; out[3].imag = -t3; out[3].real = t1; out[2].real = t1; out[0].imag = 0; out[1].imag = 0; // Reorder to the correct output order. // TODO: Modify the above computation so that this is not needed. Complex tmp = out[2]; out[2] = out[3]; out[3] = out[5]; out[5] = out[7]; out[7] = out[4]; out[4] = out[1]; out[1] = out[6]; out[6] = tmp; } // Fills in block[kBlockEdgeHalf..(kBlockHalf+kBlockEdgeHalf)], and leaves the // rest unmodified. void ButteraugliFFTSquared(double block[kBlockSize]) { double global_mul = 0.000064; Complex block_c[kBlockSize]; assert(kBlockEdge == 8); for (int y = 0; y < kBlockEdge; ++y) { RealFFT8(block + y * kBlockEdge, block_c + y * kBlockEdge); } TransposeBlock(block_c); double r0[kBlockEdge]; double r1[kBlockEdge]; for (int x = 0; x < kBlockEdge; ++x) { r0[x] = block_c[x].real; r1[x] = block_c[kBlockHalf + x].real; } RealFFT8(r0, block_c); RealFFT8(r1, block_c + kBlockHalf); for (int y = 1; y < kBlockEdgeHalf; ++y) { FFT8(block_c + y * kBlockEdge); } for (int i = kBlockEdgeHalf; i < kBlockHalf + kBlockEdgeHalf + 1; ++i) { block[i] = abssq(block_c[i]); block[i] *= global_mul; } } // Computes 8x8 FFT of each channel of xyb0 and xyb1 and adds the total squared // 3-dimensional xybdiff of the two blocks to diff_xyb_{dc,ac} and the average // diff on the edges to diff_xyb_edge_dc. void ButteraugliBlockDiff(double xyb0[3 * kBlockSize], double xyb1[3 * kBlockSize], double diff_xyb_dc[3], double diff_xyb_ac[3], double diff_xyb_edge_dc[3]) { PROFILER_FUNC; const double *csf8x8 = GetContrastSensitivityMatrix(); double avgdiff_xyb[3] = {0.0}; double avgdiff_edge[3][4] = { {0.0} }; for (int i = 0; i < 3 * kBlockSize; ++i) { const double diff_xyb = xyb0[i] - xyb1[i]; const int c = i / kBlockSize; avgdiff_xyb[c] += diff_xyb / kBlockSize; const int k = i % kBlockSize; const int kx = k % kBlockEdge; const int ky = k / kBlockEdge; const int h_edge_idx = ky == 0 ? 1 : ky == 7 ? 3 : -1; const int v_edge_idx = kx == 0 ? 0 : kx == 7 ? 2 : -1; if (h_edge_idx >= 0) { avgdiff_edge[c][h_edge_idx] += diff_xyb / kBlockEdge; } if (v_edge_idx >= 0) { avgdiff_edge[c][v_edge_idx] += diff_xyb / kBlockEdge; } } XybDiffLowFreqSquaredAccumulate(avgdiff_xyb[0], avgdiff_xyb[1], avgdiff_xyb[2], 0, 0, 0, csf8x8[0], diff_xyb_dc); for (int i = 0; i < 4; ++i) { XybDiffLowFreqSquaredAccumulate(avgdiff_edge[0][i], avgdiff_edge[1][i], avgdiff_edge[2][i], 0, 0, 0, csf8x8[0], diff_xyb_edge_dc); } double* xyb_avg = xyb0; double* xyb_halfdiff = xyb1; for(int i = 0; i < 3 * kBlockSize; ++i) { double avg = (xyb0[i] + xyb1[i])/2; double halfdiff = (xyb0[i] - xyb1[i])/2; xyb_avg[i] = avg; xyb_halfdiff[i] = halfdiff; } double *y_avg = &xyb_avg[kBlockSize]; double *x_halfdiff_squared = &xyb_halfdiff[0]; double *y_halfdiff = &xyb_halfdiff[kBlockSize]; double *z_halfdiff_squared = &xyb_halfdiff[2 * kBlockSize]; ButteraugliFFTSquared(y_avg); ButteraugliFFTSquared(x_halfdiff_squared); ButteraugliFFTSquared(y_halfdiff); ButteraugliFFTSquared(z_halfdiff_squared); static const double xmul = 64.8; static const double ymul = 1.753123908348329; static const double ymul2 = 1.51983458269; static const double zmul = 2.4; for (size_t i = kBlockEdgeHalf; i < kBlockHalf + kBlockEdgeHalf + 1; ++i) { double d = csf8x8[i]; diff_xyb_ac[0] += d * xmul * x_halfdiff_squared[i]; diff_xyb_ac[2] += d * zmul * z_halfdiff_squared[i]; y_avg[i] = sqrt(y_avg[i]); y_halfdiff[i] = sqrt(y_halfdiff[i]); double y0 = y_avg[i] - y_halfdiff[i]; double y1 = y_avg[i] + y_halfdiff[i]; // Remove the impact of small absolute values. // This improves the behavior with flat noise. static const double ylimit = 0.04; y0 = RemoveRangeAroundZero(y0, ylimit); y1 = RemoveRangeAroundZero(y1, ylimit); if (y0 != y1) { double valy0 = Interpolate(GetHighFreqColorDiffDy(), 21, y0 * ymul2); double valy1 = Interpolate(GetHighFreqColorDiffDy(), 21, y1 * ymul2); double valy = ymul * (valy0 - valy1); diff_xyb_ac[1] += d * valy * valy; } } } // Low frequency edge detectors. // Two edge detectors are applied in each corner of the 8x8 square. // The squared 3-dimensional error vector is added to diff_xyb. void Butteraugli8x8CornerEdgeDetectorDiff( const size_t pos_x, const size_t pos_y, const size_t xsize, const size_t ysize, const std::vector > &blurred0, const std::vector > &blurred1, double diff_xyb[3]) { PROFILER_FUNC; int local_count = 0; double local_xyb[3] = { 0 }; static const double w = 0.711100840192; for (int k = 0; k < 4; ++k) { size_t step = 3; size_t offset[4][2] = { { 0, 0 }, { 0, 7 }, { 7, 0 }, { 7, 7 } }; size_t x = pos_x + offset[k][0]; size_t y = pos_y + offset[k][1]; if (x >= step && x + step < xsize) { size_t ix = y * xsize + (x - step); size_t ix2 = ix + 2 * step; XybDiffLowFreqSquaredAccumulate( w * (blurred0[0][ix] - blurred0[0][ix2]), w * (blurred0[1][ix] - blurred0[1][ix2]), w * (blurred0[2][ix] - blurred0[2][ix2]), w * (blurred1[0][ix] - blurred1[0][ix2]), w * (blurred1[1][ix] - blurred1[1][ix2]), w * (blurred1[2][ix] - blurred1[2][ix2]), 1.0, local_xyb); ++local_count; } if (y >= step && y + step < ysize) { size_t ix = (y - step) * xsize + x; size_t ix2 = ix + 2 * step * xsize; XybDiffLowFreqSquaredAccumulate( w * (blurred0[0][ix] - blurred0[0][ix2]), w * (blurred0[1][ix] - blurred0[1][ix2]), w * (blurred0[2][ix] - blurred0[2][ix2]), w * (blurred1[0][ix] - blurred1[0][ix2]), w * (blurred1[1][ix] - blurred1[1][ix2]), w * (blurred1[2][ix] - blurred1[2][ix2]), 1.0, local_xyb); ++local_count; } } static const double weight = 0.01617112696; const double mul = weight * 8.0 / local_count; for (int i = 0; i < 3; ++i) { diff_xyb[i] += mul * local_xyb[i]; } } // https://en.wikipedia.org/wiki/Photopsin absordance modeling. const double *GetOpsinAbsorbance() { static const double kMix[12] = { 0.348036746003, 0.577814843137, 0.0544556093735, 0.774145581713, 0.26922717275, 0.767247733938, 0.0366922708552, 0.920130265014, 0.0882062883536, 0.158581714673, 0.712857943858, 10.6524069248, }; return &kMix[0]; } void OpsinAbsorbance(const double in[3], double out[3]) { const double *mix = GetOpsinAbsorbance(); out[0] = mix[0] * in[0] + mix[1] * in[1] + mix[2] * in[2] + mix[3]; out[1] = mix[4] * in[0] + mix[5] * in[1] + mix[6] * in[2] + mix[7]; out[2] = mix[8] * in[0] + mix[9] * in[1] + mix[10] * in[2] + mix[11]; } double GammaMinArg() { double in[3] = { 0.0, 0.0, 0.0 }; double out[3]; OpsinAbsorbance(in, out); return std::min(out[0], std::min(out[1], out[2])); } double GammaMaxArg() { double in[3] = { 255.0, 255.0, 255.0 }; double out[3]; OpsinAbsorbance(in, out); return std::max(out[0], std::max(out[1], out[2])); } ButteraugliComparator::ButteraugliComparator( size_t xsize, size_t ysize, int step) : xsize_(xsize), ysize_(ysize), num_pixels_(xsize * ysize), step_(step), res_xsize_((xsize + step - 1) / step), res_ysize_((ysize + step - 1) / step) { assert(step <= 4); } void MaskHighIntensityChange( size_t xsize, size_t ysize, const std::vector > &c0, const std::vector > &c1, std::vector > &xyb0, std::vector > &xyb1) { PROFILER_FUNC; for (int y = 0; y < ysize; ++y) { for (int x = 0; x < xsize; ++x) { int ix = y * xsize + x; const double ave[3] = { (c0[0][ix] + c1[0][ix]) * 0.5, (c0[1][ix] + c1[1][ix]) * 0.5, (c0[2][ix] + c1[2][ix]) * 0.5, }; double sqr_max_diff = -1; { int offset[4] = { -1, 1, -static_cast(xsize), static_cast(xsize) }; int border[4] = { x == 0, x + 1 == xsize, y == 0, y + 1 == ysize }; for (int dir = 0; dir < 4; ++dir) { if (border[dir]) { continue; } const int ix2 = ix + offset[dir]; double diff = 0.5 * (c0[1][ix2] + c1[1][ix2]) - ave[1]; diff *= diff; if (sqr_max_diff < diff) { sqr_max_diff = diff; } } } static const double kReductionX = 275.19165240059317; static const double kReductionY = 18599.41286306991; static const double kReductionZ = 410.8995306951065; static const double kChromaBalance = 106.95800948271017; double chroma_scale = kChromaBalance / (ave[1] + kChromaBalance); const double mix[3] = { chroma_scale * kReductionX / (sqr_max_diff + kReductionX), kReductionY / (sqr_max_diff + kReductionY), chroma_scale * kReductionZ / (sqr_max_diff + kReductionZ), }; // Interpolate lineraly between the average color and the actual // color -- to reduce the importance of this pixel. for (int i = 0; i < 3; ++i) { xyb0[i][ix] = mix[i] * c0[i][ix] + (1 - mix[i]) * ave[i]; xyb1[i][ix] = mix[i] * c1[i][ix] + (1 - mix[i]) * ave[i]; } } } } double SimpleGamma(double v) { static const double kGamma = 0.387494322593; static const double limit = 43.01745241042018; double bright = v - limit; if (bright >= 0) { static const double mul = 0.0383723643799; v -= bright * mul; } static const double limit2 = 94.68634353321337; double bright2 = v - limit2; if (bright2 >= 0) { static const double mul = 0.22885405968; v -= bright2 * mul; } static const double offset = 0.156775786057; static const double scale = 8.898059160493739; double retval = scale * (offset + pow(v, kGamma)); return retval; } static inline double Gamma(double v) { // return SimpleGamma(v); return GammaPolynomial(v); } void OpsinDynamicsImage(size_t xsize, size_t ysize, std::vector > &rgb) { PROFILER_FUNC; std::vector > blurred = rgb; static const double kSigma = 1.1; for (int i = 0; i < 3; ++i) { Blur(xsize, ysize, blurred[i].data(), kSigma, 0.0); } for (int i = 0; i < rgb[0].size(); ++i) { double sensitivity[3]; { // Calculate sensitivity[3] based on the smoothed image gamma derivative. double pre_rgb[3] = { blurred[0][i], blurred[1][i], blurred[2][i] }; double pre_mixed[3]; OpsinAbsorbance(pre_rgb, pre_mixed); sensitivity[0] = Gamma(pre_mixed[0]) / pre_mixed[0]; sensitivity[1] = Gamma(pre_mixed[1]) / pre_mixed[1]; sensitivity[2] = Gamma(pre_mixed[2]) / pre_mixed[2]; } double cur_rgb[3] = { rgb[0][i], rgb[1][i], rgb[2][i] }; double cur_mixed[3]; OpsinAbsorbance(cur_rgb, cur_mixed); cur_mixed[0] *= sensitivity[0]; cur_mixed[1] *= sensitivity[1]; cur_mixed[2] *= sensitivity[2]; double x, y, z; RgbToXyb(cur_mixed[0], cur_mixed[1], cur_mixed[2], &x, &y, &z); rgb[0][i] = x; rgb[1][i] = y; rgb[2][i] = z; } } static void ScaleImage(double scale, std::vector *result) { PROFILER_FUNC; for (size_t i = 0; i < result->size(); ++i) { (*result)[i] *= scale; } } // Making a cluster of local errors to be more impactful than // just a single error. void CalculateDiffmap(const size_t xsize, const size_t ysize, const int step, std::vector* diffmap) { PROFILER_FUNC; // Shift the diffmap more correctly above the pixels, from 2.5 pixels to 0.5 // pixels distance over the original image. The border of 2 pixels on top and // left side and 3 pixels on right and bottom side are zeroed, but these // values have no meaning, they only exist to keep the result map the same // size as the input images. int s2 = (8 - step) / 2; // Upsample and take square root. std::vector diffmap_out(xsize * ysize); const size_t res_xsize = (xsize + step - 1) / step; for (size_t res_y = 0; res_y + 8 - step < ysize; res_y += step) { for (size_t res_x = 0; res_x + 8 - step < xsize; res_x += step) { size_t res_ix = (res_y * res_xsize + res_x) / step; float orig_val = (*diffmap)[res_ix]; constexpr float kInitialSlope = 100; // TODO(b/29974893): Until that is fixed do not call sqrt on very small // numbers. double val = orig_val < (1.0 / (kInitialSlope * kInitialSlope)) ? kInitialSlope * orig_val : std::sqrt(orig_val); for (size_t off_y = 0; off_y < step; ++off_y) { for (size_t off_x = 0; off_x < step; ++off_x) { diffmap_out[(res_y + off_y + s2) * xsize + res_x + off_x + s2] = val; } } } } *diffmap = diffmap_out; { static const double kSigma = 8.8510880283; static const double mul1 = 24.8235314874; static const double scale = 1.0 / (1.0 + mul1); const int s = 8 - step; std::vector blurred((xsize - s) * (ysize - s)); for (int y = 0; y < ysize - s; ++y) { for (int x = 0; x < xsize - s; ++x) { blurred[y * (xsize - s) + x] = (*diffmap)[(y + s2) * xsize + x + s2]; } } static const double border_ratio = 0.03027655136; Blur(xsize - s, ysize - s, blurred.data(), kSigma, border_ratio); for (int y = 0; y < ysize - s; ++y) { for (int x = 0; x < xsize - s; ++x) { (*diffmap)[(y + s2) * xsize + x + s2] += mul1 * blurred[y * (xsize - s) + x]; } } ScaleImage(scale, diffmap); } } void ButteraugliComparator::Diffmap(const std::vector &rgb0_arg, const std::vector &rgb1_arg, ImageF &result) { result = ImageF(xsize_, ysize_); if (xsize_ < 8 || ysize_ < 8) return; std::vector> rgb0_c = PackedFromPlanes(rgb0_arg); std::vector> rgb1_c = PackedFromPlanes(rgb1_arg); OpsinDynamicsImage(xsize_, ysize_, rgb0_c); OpsinDynamicsImage(xsize_, ysize_, rgb1_c); std::vector pg0 = PlanesFromPacked(xsize_, ysize_, rgb0_c); std::vector pg1 = PlanesFromPacked(xsize_, ysize_, rgb1_c); DiffmapOpsinDynamicsImage(pg0, pg1, result); } void ButteraugliComparator::DiffmapOpsinDynamicsImage( const std::vector &xyb0_arg, const std::vector &xyb1_arg, ImageF &result) { result = ImageF(xsize_, ysize_); if (xsize_ < 8 || ysize_ < 8) return; std::vector> xyb0 = PackedFromPlanes(xyb0_arg); std::vector> xyb1 = PackedFromPlanes(xyb1_arg); auto xyb0_c = xyb0; auto xyb1_c = xyb1; MaskHighIntensityChange(xsize_, ysize_, xyb0_c, xyb1_c, xyb0, xyb1); assert(8 <= xsize_); for (int i = 0; i < 3; i++) { assert(xyb0[i].size() == num_pixels_); assert(xyb1[i].size() == num_pixels_); } std::vector > mask_xyb(3); std::vector > mask_xyb_dc(3); std::vector block_diff_dc(3 * res_xsize_ * res_ysize_); std::vector block_diff_ac(3 * res_xsize_ * res_ysize_); std::vector edge_detector_map(3 * res_xsize_ * res_ysize_); std::vector packed_result; BlockDiffMap(xyb0, xyb1, &block_diff_dc, &block_diff_ac); EdgeDetectorMap(xyb0, xyb1, &edge_detector_map); EdgeDetectorLowFreq(xyb0, xyb1, &block_diff_ac); Mask(xyb0, xyb1, xsize_, ysize_, &mask_xyb, &mask_xyb_dc); CombineChannels(mask_xyb, mask_xyb_dc, block_diff_dc, block_diff_ac, edge_detector_map, &packed_result); CalculateDiffmap(xsize_, ysize_, step_, &packed_result); CopyFromPacked(packed_result, &result); } void ButteraugliComparator::BlockDiffMap( const std::vector > &xyb0, const std::vector > &xyb1, std::vector* block_diff_dc, std::vector* block_diff_ac) { PROFILER_FUNC; for (size_t res_y = 0; res_y + (kBlockEdge - step_ - 1) < ysize_; res_y += step_) { for (size_t res_x = 0; res_x + (kBlockEdge - step_ - 1) < xsize_; res_x += step_) { size_t res_ix = (res_y * res_xsize_ + res_x) / step_; size_t offset = (std::min(res_y, ysize_ - 8) * xsize_ + std::min(res_x, xsize_ - 8)); double block0[3 * kBlockEdge * kBlockEdge]; double block1[3 * kBlockEdge * kBlockEdge]; for (int i = 0; i < 3; ++i) { double *m0 = &block0[i * kBlockEdge * kBlockEdge]; double *m1 = &block1[i * kBlockEdge * kBlockEdge]; for (size_t y = 0; y < kBlockEdge; y++) { for (size_t x = 0; x < kBlockEdge; x++) { m0[kBlockEdge * y + x] = xyb0[i][offset + y * xsize_ + x]; m1[kBlockEdge * y + x] = xyb1[i][offset + y * xsize_ + x]; } } } double diff_xyb_dc[3] = { 0.0 }; double diff_xyb_ac[3] = { 0.0 }; double diff_xyb_edge_dc[3] = { 0.0 }; ButteraugliBlockDiff(block0, block1, diff_xyb_dc, diff_xyb_ac, diff_xyb_edge_dc); for (int i = 0; i < 3; ++i) { (*block_diff_dc)[3 * res_ix + i] = diff_xyb_dc[i]; (*block_diff_ac)[3 * res_ix + i] = diff_xyb_ac[i]; } } } } void ButteraugliComparator::EdgeDetectorMap( const std::vector > &xyb0, const std::vector > &xyb1, std::vector* edge_detector_map) { PROFILER_FUNC; static const double kSigma[3] = { 1.5, 0.586, 0.4, }; std::vector > blurred0(xyb0); std::vector > blurred1(xyb1); for (int i = 0; i < 3; i++) { Blur(xsize_, ysize_, blurred0[i].data(), kSigma[i], 0.0); Blur(xsize_, ysize_, blurred1[i].data(), kSigma[i], 0.0); } for (size_t res_y = 0; res_y + (8 - step_) < ysize_; res_y += step_) { for (size_t res_x = 0; res_x + (8 - step_) < xsize_; res_x += step_) { size_t res_ix = (res_y * res_xsize_ + res_x) / step_; double diff_xyb[3] = { 0.0 }; Butteraugli8x8CornerEdgeDetectorDiff(std::min(res_x, xsize_ - 8), std::min(res_y, ysize_ - 8), xsize_, ysize_, blurred0, blurred1, diff_xyb); for (int i = 0; i < 3; ++i) { (*edge_detector_map)[3 * res_ix + i] = diff_xyb[i]; } } } } void ButteraugliComparator::EdgeDetectorLowFreq( const std::vector > &xyb0, const std::vector > &xyb1, std::vector* block_diff_ac) { PROFILER_FUNC; static const double kSigma = 14; static const double kMul = 10; std::vector > blurred0(xyb0); std::vector > blurred1(xyb1); for (int i = 0; i < 3; i++) { Blur(xsize_, ysize_, blurred0[i].data(), kSigma, 0.0); Blur(xsize_, ysize_, blurred1[i].data(), kSigma, 0.0); } const int step = 8; for (int y = 0; y + step < ysize_; y += step_) { int resy = y / step_; int resx = step / step_; for (int x = 0; x + step < xsize_; x += step_, resx++) { const int ix = y * xsize_ + x; const int res_ix = resy * res_xsize_ + resx; double diff[4][3]; for (int i = 0; i < 3; ++i) { int ix2 = ix + 8; diff[0][i] = ((blurred1[i][ix] - blurred0[i][ix]) + (blurred0[i][ix2] - blurred1[i][ix2])); ix2 = ix + 8 * xsize_; diff[1][i] = ((blurred1[i][ix] - blurred0[i][ix]) + (blurred0[i][ix2] - blurred1[i][ix2])); ix2 = ix + 6 * xsize_ + 6; diff[2][i] = ((blurred1[i][ix] - blurred0[i][ix]) + (blurred0[i][ix2] - blurred1[i][ix2])); ix2 = ix + 6 * xsize_ - 6; diff[3][i] = x < step ? 0 : ((blurred1[i][ix] - blurred0[i][ix]) + (blurred0[i][ix2] - blurred1[i][ix2])); } double max_diff_xyb[3] = { 0 }; for (int k = 0; k < 4; ++k) { double diff_xyb[3] = { 0 }; XybDiffLowFreqSquaredAccumulate(diff[k][0], diff[k][1], diff[k][2], 0, 0, 0, 1.0, diff_xyb); for (int i = 0; i < 3; ++i) { max_diff_xyb[i] = std::max(max_diff_xyb[i], diff_xyb[i]); } } for (int i = 0; i < 3; ++i) { (*block_diff_ac)[3 * res_ix + i] += kMul * max_diff_xyb[i]; } } } } void ButteraugliComparator::CombineChannels( const std::vector >& mask_xyb, const std::vector >& mask_xyb_dc, const std::vector& block_diff_dc, const std::vector& block_diff_ac, const std::vector& edge_detector_map, std::vector* result) { PROFILER_FUNC; result->resize(res_xsize_ * res_ysize_); for (size_t res_y = 0; res_y + (8 - step_) < ysize_; res_y += step_) { for (size_t res_x = 0; res_x + (8 - step_) < xsize_; res_x += step_) { size_t res_ix = (res_y * res_xsize_ + res_x) / step_; double mask[3]; double dc_mask[3]; for (int i = 0; i < 3; ++i) { mask[i] = mask_xyb[i][(res_y + 3) * xsize_ + (res_x + 3)]; dc_mask[i] = mask_xyb_dc[i][(res_y + 3) * xsize_ + (res_x + 3)]; } (*result)[res_ix] = (DotProduct(&block_diff_dc[3 * res_ix], dc_mask) + DotProduct(&block_diff_ac[3 * res_ix], mask) + DotProduct(&edge_detector_map[3 * res_ix], mask)); } } } double ButteraugliScoreFromDiffmap(const ImageF& diffmap) { PROFILER_FUNC; float retval = 0.0f; for (size_t y = 0; y < diffmap.ysize(); ++y) { ConstRestrict row = diffmap.Row(y); for (size_t x = 0; x < diffmap.xsize(); ++x) { retval = std::max(retval, row[x]); } } return retval; } static std::array MakeMask( double extmul, double extoff, double mul, double offset, double scaler) { std::array lut; for (int i = 0; i < lut.size(); ++i) { const double c = mul / ((0.01 * scaler * i) + offset); lut[i] = 1.0 + extmul * (c + extoff); assert(lut[i] >= 0.0); lut[i] *= lut[i]; } return lut; } double MaskX(double delta) { PROFILER_FUNC; static const double extmul = 0.975741017749; static const double extoff = -4.25328244168; static const double offset = 0.454909521427; static const double scaler = 0.0738288224836; static const double mul = 20.8029176447; static const std::array lut = MakeMask(extmul, extoff, mul, offset, scaler); return InterpolateClampNegative(lut.data(), lut.size(), delta); } double MaskY(double delta) { PROFILER_FUNC; static const double extmul = 0.373995618954; static const double extoff = 1.5307267433; static const double offset = 0.911952641929; static const double scaler = 1.1731667845; static const double mul = 16.2447033988; static const std::array lut = MakeMask(extmul, extoff, mul, offset, scaler); return InterpolateClampNegative(lut.data(), lut.size(), delta); } double MaskB(double delta) { PROFILER_FUNC; static const double extmul = 0.61582234137; static const double extoff = -4.25376118646; static const double offset = 1.05105070921; static const double scaler = 0.47434643535; static const double mul = 31.1444967089; static const std::array lut = MakeMask(extmul, extoff, mul, offset, scaler); return InterpolateClampNegative(lut.data(), lut.size(), delta); } double MaskDcX(double delta) { PROFILER_FUNC; static const double extmul = 1.79116943438; static const double extoff = -3.86797479189; static const double offset = 0.670960225853; static const double scaler = 0.486575865525; static const double mul = 20.4563479139; static const std::array lut = MakeMask(extmul, extoff, mul, offset, scaler); return InterpolateClampNegative(lut.data(), lut.size(), delta); } double MaskDcY(double delta) { PROFILER_FUNC; static const double extmul = 0.212223514236; static const double extoff = -3.65647120524; static const double offset = 1.73396799447; static const double scaler = 0.170392660501; static const double mul = 21.6566724788; static const std::array lut = MakeMask(extmul, extoff, mul, offset, scaler); return InterpolateClampNegative(lut.data(), lut.size(), delta); } double MaskDcB(double delta) { PROFILER_FUNC; static const double extmul = 0.349376011816; static const double extoff = -0.894711072781; static const double offset = 0.901647926679; static const double scaler = 0.380086095024; static const double mul = 18.0373825149; static const std::array lut = MakeMask(extmul, extoff, mul, offset, scaler); return InterpolateClampNegative(lut.data(), lut.size(), delta); } // Replaces values[x + y * xsize] with the minimum of the values in the // square_size square with coordinates // x - offset .. x + square_size - offset - 1, // y - offset .. y + square_size - offset - 1. void MinSquareVal(size_t square_size, size_t offset, size_t xsize, size_t ysize, float *values) { PROFILER_FUNC; // offset is not negative and smaller than square_size. assert(offset < square_size); std::vector tmp(xsize * ysize); for (size_t y = 0; y < ysize; ++y) { const size_t minh = offset > y ? 0 : y - offset; const size_t maxh = std::min(ysize, y + square_size - offset); for (size_t x = 0; x < xsize; ++x) { double min = values[x + minh * xsize]; for (size_t j = minh + 1; j < maxh; ++j) { min = fmin(min, values[x + j * xsize]); } tmp[x + y * xsize] = min; } } for (size_t x = 0; x < xsize; ++x) { const size_t minw = offset > x ? 0 : x - offset; const size_t maxw = std::min(xsize, x + square_size - offset); for (size_t y = 0; y < ysize; ++y) { double min = tmp[minw + y * xsize]; for (size_t j = minw + 1; j < maxw; ++j) { min = fmin(min, tmp[j + y * xsize]); } values[x + y * xsize] = min; } } } // ===== Functions used by Mask only ===== void Average5x5(int xsize, int ysize, std::vector* diffs) { PROFILER_FUNC; if (xsize < 4 || ysize < 4) { // TODO: Make this work for small dimensions as well. return; } static const float w = 0.679144890667; static const float scale = 1.0 / (5.0 + 4 * w); std::vector result = *diffs; std::vector tmp0 = *diffs; std::vector tmp1 = *diffs; ScaleImage(w, &tmp1); for (int y = 0; y < ysize; y++) { const int row0 = y * xsize; result[row0 + 1] += tmp0[row0]; result[row0 + 0] += tmp0[row0 + 1]; result[row0 + 2] += tmp0[row0 + 1]; for (int x = 2; x < xsize - 2; ++x) { result[row0 + x - 1] += tmp0[row0 + x]; result[row0 + x + 1] += tmp0[row0 + x]; } result[row0 + xsize - 3] += tmp0[row0 + xsize - 2]; result[row0 + xsize - 1] += tmp0[row0 + xsize - 2]; result[row0 + xsize - 2] += tmp0[row0 + xsize - 1]; if (y > 0) { const int rowd1 = row0 - xsize; result[rowd1 + 1] += tmp1[row0]; result[rowd1 + 0] += tmp0[row0]; for (int x = 1; x < xsize - 1; ++x) { result[rowd1 + x + 1] += tmp1[row0 + x]; result[rowd1 + x + 0] += tmp0[row0 + x]; result[rowd1 + x - 1] += tmp1[row0 + x]; } result[rowd1 + xsize - 1] += tmp0[row0 + xsize - 1]; result[rowd1 + xsize - 2] += tmp1[row0 + xsize - 1]; } if (y + 1 < ysize) { const int rowu1 = row0 + xsize; result[rowu1 + 1] += tmp1[row0]; result[rowu1 + 0] += tmp0[row0]; for (int x = 1; x < xsize - 1; ++x) { result[rowu1 + x + 1] += tmp1[row0 + x]; result[rowu1 + x + 0] += tmp0[row0 + x]; result[rowu1 + x - 1] += tmp1[row0 + x]; } result[rowu1 + xsize - 1] += tmp0[row0 + xsize - 1]; result[rowu1 + xsize - 2] += tmp1[row0 + xsize - 1]; } } *diffs = result; ScaleImage(scale, diffs); } void DiffPrecompute( const std::vector > &xyb0, const std::vector > &xyb1, size_t xsize, size_t ysize, std::vector > *mask) { PROFILER_FUNC; mask->resize(3, std::vector(xyb0[0].size())); double valsh0[3] = { 0.0 }; double valsv0[3] = { 0.0 }; double valsh1[3] = { 0.0 }; double valsv1[3] = { 0.0 }; int ix2; for (size_t y = 0; y < ysize; ++y) { for (size_t x = 0; x < xsize; ++x) { size_t ix = x + xsize * y; if (x + 1 < xsize) { ix2 = ix + 1; } else { ix2 = ix - 1; } { double x0 = (xyb0[0][ix] - xyb0[0][ix2]); double y0 = (xyb0[1][ix] - xyb0[1][ix2]); double z0 = (xyb0[2][ix] - xyb0[2][ix2]); XybToVals(x0, y0, z0, &valsh0[0], &valsh0[1], &valsh0[2]); double x1 = (xyb1[0][ix] - xyb1[0][ix2]); double y1 = (xyb1[1][ix] - xyb1[1][ix2]); double z1 = (xyb1[2][ix] - xyb1[2][ix2]); XybToVals(x1, y1, z1, &valsh1[0], &valsh1[1], &valsh1[2]); } if (y + 1 < ysize) { ix2 = ix + xsize; } else { ix2 = ix - xsize; } { double x0 = (xyb0[0][ix] - xyb0[0][ix2]); double y0 = (xyb0[1][ix] - xyb0[1][ix2]); double z0 = (xyb0[2][ix] - xyb0[2][ix2]); XybToVals(x0, y0, z0, &valsv0[0], &valsv0[1], &valsv0[2]); double x1 = (xyb1[0][ix] - xyb1[0][ix2]); double y1 = (xyb1[1][ix] - xyb1[1][ix2]); double z1 = (xyb1[2][ix] - xyb1[2][ix2]); XybToVals(x1, y1, z1, &valsv1[0], &valsv1[1], &valsv1[2]); } for (int i = 0; i < 3; ++i) { double sup0 = fabs(valsh0[i]) + fabs(valsv0[i]); double sup1 = fabs(valsh1[i]) + fabs(valsv1[i]); double m = std::min(sup0, sup1); (*mask)[i][ix] = m; } } } } void Mask(const std::vector > &xyb0, const std::vector > &xyb1, size_t xsize, size_t ysize, std::vector > *mask, std::vector > *mask_dc) { PROFILER_FUNC; mask->resize(3); mask_dc->resize(3); for (int i = 0; i < 3; ++i) { (*mask)[i].resize(xsize * ysize); (*mask_dc)[i].resize(xsize * ysize); } DiffPrecompute(xyb0, xyb1, xsize, ysize, mask); for (int i = 0; i < 3; ++i) { Average5x5(xsize, ysize, &(*mask)[i]); MinSquareVal(4, 0, xsize, ysize, (*mask)[i].data()); static const double sigma[3] = { 9.65781083553, 14.2644604355, 4.53358927369, }; Blur(xsize, ysize, (*mask)[i].data(), sigma[i], 0.0); } static const double w00 = 232.206464018; static const double w11 = 22.9455222245; static const double w22 = 503.962310606; for (size_t y = 0; y < ysize; ++y) { for (size_t x = 0; x < xsize; ++x) { const size_t idx = y * xsize + x; const double s0 = (*mask)[0][idx]; const double s1 = (*mask)[1][idx]; const double s2 = (*mask)[2][idx]; const double p0 = w00 * s0; const double p1 = w11 * s1; const double p2 = w22 * s2; (*mask)[0][idx] = MaskX(p0); (*mask)[1][idx] = MaskY(p1); (*mask)[2][idx] = MaskB(p2); (*mask_dc)[0][idx] = MaskDcX(p0); (*mask_dc)[1][idx] = MaskDcY(p1); (*mask_dc)[2][idx] = MaskDcB(p2); } } for (int i = 0; i < 3; ++i) { ScaleImage(kGlobalScale * kGlobalScale, &(*mask)[i]); ScaleImage(kGlobalScale * kGlobalScale, &(*mask_dc)[i]); } } void ButteraugliDiffmap(const std::vector &rgb0_image, const std::vector &rgb1_image, ImageF &result_image) { const size_t xsize = rgb0_image[0].xsize(); const size_t ysize = rgb0_image[0].ysize(); ButteraugliComparator butteraugli(xsize, ysize, 3); butteraugli.Diffmap(rgb0_image, rgb1_image, result_image); } bool ButteraugliInterface(const std::vector &rgb0, const std::vector &rgb1, ImageF &diffmap, double &diffvalue) { const size_t xsize = rgb0[0].xsize(); const size_t ysize = rgb0[0].ysize(); if (xsize < 1 || ysize < 1) { // Butteraugli values for small (where xsize or ysize is smaller // than 8 pixels) images are non-sensical, but most likely it is // less disruptive to try to compute something than just give up. return false; // No image. } for (int i = 1; i < 3; i++) { if (rgb0[i].xsize() != xsize || rgb0[i].ysize() != ysize || rgb1[i].xsize() != xsize || rgb1[i].ysize() != ysize) { return false; // Image planes must have same dimensions. } } if (xsize < 8 || ysize < 8) { for (int y = 0; y < ysize; ++y) { for (int x = 0; x < xsize; ++x) { diffmap.Row(y)[x] = 0; } } diffvalue = 0; return true; } ButteraugliDiffmap(rgb0, rgb1, diffmap); diffvalue = ButteraugliScoreFromDiffmap(diffmap); return true; } bool ButteraugliAdaptiveQuantization(size_t xsize, size_t ysize, const std::vector > &rgb, std::vector &quant) { if (xsize < 16 || ysize < 16) { return false; // Butteraugli is undefined for small images. } size_t size = xsize * ysize; std::vector > scale_xyb(3); std::vector > scale_xyb_dc(3); Mask(rgb, rgb, xsize, ysize, &scale_xyb, &scale_xyb_dc); quant.resize(size); // Mask gives us values in 3 color channels, but for now we take only // the intensity channel. for (size_t i = 0; i < size; i++) { quant[i] = scale_xyb[1][i]; } return true; } double ButteraugliFuzzyClass(double score) { static const double fuzzy_width_up = 10.287189655; static const double fuzzy_width_down = 6.97490803335; static const double m0 = 2.0; double fuzzy_width = score < 1.0 ? fuzzy_width_down : fuzzy_width_up; return m0 / (1.0 + exp((score - 1.0) * fuzzy_width)); } double ButteraugliFuzzyInverse(double seek) { double pos = 0; for (double range = 1.0; range >= 1e-10; range *= 0.5) { double cur = ButteraugliFuzzyClass(pos); if (cur < seek) { pos -= range; } else { pos += range; } } return pos; } } // namespace butteraugli