// Copyright (c) the JPEG XL Project Authors. All rights reserved. // // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // // 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
// right border for (size_t x = border2; x < in.xsize(); ++x) {
ConvolveBorderColumn(in, kernel, x, out->Row(x));
} returntrue;
}
// A blur somewhat similar to a 2D Gaussian blur. // See: https://en.wikipedia.org/wiki/Gaussian_blur // // This is a bottleneck because the sigma can be quite large (>7). We can use // gauss_blur.cc (runtime independent of sigma, closer to a 4*sigma truncated // Gaussian and our 2.25 in ComputeKernel), but its boundary conditions are // zero-valued. This leads to noticeable differences at the edges of diffmaps. // We retain a special case for 5x5 kernels (even faster than gauss_blur), // optionally use gauss_blur followed by fixup of the borders for large images, // or fall back to the previous truncated FIR followed by a transpose.
Status Blur(const ImageF& in, float sigma, const ButteraugliParams& params,
BlurTemp* temp, ImageF* out) {
std::vector<float> kernel = ComputeKernel(sigma); // Separable5 does an in-place convolution, so this fast path is not safe if // in aliases out. if (kernel.size() == 5 && &in != out) { float sum_weights = 0.0f; for (constfloat w : kernel) {
sum_weights += w;
} constfloat scale = 1.0f / sum_weights; constfloat w0 = kernel[2] * scale; constfloat w1 = kernel[1] * scale; constfloat w2 = kernel[0] * scale; const WeightsSeparable5 weights = {
{HWY_REP4(w0), HWY_REP4(w1), HWY_REP4(w2)},
{HWY_REP4(w0), HWY_REP4(w1), HWY_REP4(w2)},
};
JXL_RETURN_IF_ERROR(
Separable5(in, Rect(in), weights, /*pool=*/nullptr, out)); returntrue;
}
// These templates are not found via ADL. using hwy::HWY_NAMESPACE::Abs; using hwy::HWY_NAMESPACE::Div; using hwy::HWY_NAMESPACE::Gt; using hwy::HWY_NAMESPACE::IfThenElse; using hwy::HWY_NAMESPACE::IfThenElseZero; using hwy::HWY_NAMESPACE::Lt; using hwy::HWY_NAMESPACE::Max; using hwy::HWY_NAMESPACE::Mul; using hwy::HWY_NAMESPACE::MulAdd; using hwy::HWY_NAMESPACE::MulSub; using hwy::HWY_NAMESPACE::Neg; using hwy::HWY_NAMESPACE::Sub; using hwy::HWY_NAMESPACE::Vec; using hwy::HWY_NAMESPACE::ZeroIfNegative;
template <class D, class V>
HWY_INLINE V MaximumClamp(D d, V v, double kMaxVal) { staticconstdouble kMul = 0.724216145665; const V mul = Set(d, kMul); const V maxval = Set(d, kMaxVal); // If greater than maxval or less than -maxval, replace with if_*. const V if_pos = MulAdd(Sub(v, maxval), mul, maxval); const V if_neg = MulSub(Add(v, maxval), mul, maxval); const V pos_or_v = IfThenElse(Ge(v, maxval), if_pos, v); return IfThenElse(Lt(v, Neg(maxval)), if_neg, pos_or_v);
}
// Make area around zero less important (remove it). template <class D, class V>
HWY_INLINE V RemoveRangeAroundZero(const D d, constdouble kw, const V x) { constauto w = Set(d, kw); return IfThenElse(Gt(x, w), Sub(x, w),
IfThenElseZero(Lt(x, Neg(w)), Add(x, w)));
}
// Make area around zero more important (2x it until the limit). template <class D, class V>
HWY_INLINE V AmplifyRangeAroundZero(const D d, constdouble kw, const V x) { constauto w = Set(d, kw); return IfThenElse(Gt(x, w), Add(x, w),
IfThenElse(Lt(x, Neg(w)), Sub(x, w), Add(x, x)));
}
// XybLowFreqToVals converts from low-frequency XYB space to the 'vals' space. // Vals space can be converted to L2-norm space (Euclidean and normalized) // through visual masking. template <class D, class V>
HWY_INLINE void XybLowFreqToVals(const D d, const V& x, const V& y, const V& b_arg, V* HWY_RESTRICT valx,
V* HWY_RESTRICT valy, V* HWY_RESTRICT valb) { staticconstdouble xmul_scalar = 33.832837186260; staticconstdouble ymul_scalar = 14.458268100570; staticconstdouble bmul_scalar = 49.87984651440; staticconstdouble y_to_b_mul_scalar = -0.362267051518; const V xmul = Set(d, xmul_scalar); const V ymul = Set(d, ymul_scalar); const V bmul = Set(d, bmul_scalar); const V y_to_b_mul = Set(d, y_to_b_mul_scalar); const V b = MulAdd(y_to_b_mul, y, b_arg);
*valb = Mul(b, bmul);
*valx = Mul(x, xmul);
*valy = Mul(y, ymul);
}
void XybLowFreqToVals(Image3F* xyb_lf) { // Modify range around zero code only concerns the high frequency // planes and only the X and Y channels. // Convert low freq xyb to vals space so that we can do a simple squared sum // diff on the low frequencies later. const HWY_FULL(float) d; for (size_t y = 0; y < xyb_lf->ysize(); ++y) { float* BUTTERAUGLI_RESTRICT row_x = xyb_lf->PlaneRow(0, y); float* BUTTERAUGLI_RESTRICT row_y = xyb_lf->PlaneRow(1, y); float* BUTTERAUGLI_RESTRICT row_b = xyb_lf->PlaneRow(2, y); for (size_t x = 0; x < xyb_lf->xsize(); x += Lanes(d)) { auto valx = Undefined(d); auto valy = Undefined(d); auto valb = Undefined(d);
XybLowFreqToVals(d, Load(d, row_x + x), Load(d, row_y + x),
Load(d, row_b + x), &valx, &valy, &valb);
Store(valx, d, row_x + x);
Store(valy, d, row_y + x);
Store(valb, d, row_b + x);
}
}
}
namespace { template <typename V>
BUTTERAUGLI_INLINE V Sum(V a, V b, V c, V d) { return Add(Add(a, b), Add(c, d));
} template <typename V>
BUTTERAUGLI_INLINE V Sum(V a, V b, V c, V d, V e) { return Sum(a, b, c, Add(d, e));
} template <typename V>
BUTTERAUGLI_INLINE V Sum(V a, V b, V c, V d, V e, V f, V g) { return Sum(a, b, c, Sum(d, e, f, g));
} template <typename V>
BUTTERAUGLI_INLINE V Sum(V a, V b, V c, V d, V e, V f, V g, V h, V i) { return Add(Add(Sum(a, b, c, d), Sum(e, f, g, h)), i);
}
} // namespace
// x grows, y constant constauto sum_yconst =
Sum(LoadU(df, d - 4), LoadU(df, d - 3), LoadU(df, d - 2),
LoadU(df, d - 1), center, LoadU(df, d + 1), LoadU(df, d + 2),
LoadU(df, d + 3), LoadU(df, d + 4)); // Will return this, sum of all line kernels auto retval = Mul(sum_yconst, sum_yconst);
{ // y grows, x constant auto sum = Sum(LoadU(df, d - xs3 - xs), LoadU(df, d - xs3),
LoadU(df, d - xs - xs), LoadU(df, d - xs), center,
LoadU(df, d + xs), LoadU(df, d + xs + xs),
LoadU(df, d + xs3), LoadU(df, d + xs3 + xs));
retval = MulAdd(sum, sum, retval);
}
{ // both grow auto sum = Sum(LoadU(df, d - xs3 - 3), LoadU(df, d - xs - xs - 2),
LoadU(df, d - xs - 1), center, LoadU(df, d + xs + 1),
LoadU(df, d + xs + xs + 2), LoadU(df, d + xs3 + 3));
retval = MulAdd(sum, sum, retval);
}
{ // y grows, x shrinks auto sum = Sum(LoadU(df, d - xs3 + 3), LoadU(df, d - xs - xs + 2),
LoadU(df, d - xs + 1), center, LoadU(df, d + xs - 1),
LoadU(df, d + xs + xs - 2), LoadU(df, d + xs3 - 3));
retval = MulAdd(sum, sum, retval);
}
{ // y grows -4 to 4, x shrinks 1 -> -1 auto sum = Sum(LoadU(df, d - xs3 - xs + 1), LoadU(df, d - xs3 + 1),
LoadU(df, d - xs - xs + 1), LoadU(df, d - xs), center,
LoadU(df, d + xs), LoadU(df, d + xs + xs - 1),
LoadU(df, d + xs3 - 1), LoadU(df, d + xs3 + xs - 1));
retval = MulAdd(sum, sum, retval);
}
{ // y grows -4 to 4, x grows -1 -> 1 auto sum = Sum(LoadU(df, d - xs3 - xs - 1), LoadU(df, d - xs3 - 1),
LoadU(df, d - xs - xs - 1), LoadU(df, d - xs), center,
LoadU(df, d + xs), LoadU(df, d + xs + xs + 1),
LoadU(df, d + xs3 + 1), LoadU(df, d + xs3 + xs + 1));
retval = MulAdd(sum, sum, retval);
}
{ // x grows -4 to 4, y grows -1 to 1 auto sum =
Sum(LoadU(df, d - 4 - xs), LoadU(df, d - 3 - xs), LoadU(df, d - 2 - xs),
LoadU(df, d - 1), center, LoadU(df, d + 1), LoadU(df, d + 2 + xs),
LoadU(df, d + 3 + xs), LoadU(df, d + 4 + xs));
retval = MulAdd(sum, sum, retval);
}
{ // x grows -4 to 4, y shrinks 1 to -1 auto sum =
Sum(LoadU(df, d - 4 + xs), LoadU(df, d - 3 + xs), LoadU(df, d - 2 + xs),
LoadU(df, d - 1), center, LoadU(df, d + 1), LoadU(df, d + 2 - xs),
LoadU(df, d + 3 - xs), LoadU(df, d + 4 - xs));
retval = MulAdd(sum, sum, retval);
}
{ /* 0_________ 1__*______ 2___*_____ 3___*_____ 4____0____ 5_____*___ 6_____*___ 7______*__
8_________ */ auto sum = Sum(LoadU(df, d - xs3 - 2), LoadU(df, d - xs - xs - 1),
LoadU(df, d - xs - 1), center, LoadU(df, d + xs + 1),
LoadU(df, d + xs + xs + 1), LoadU(df, d + xs3 + 2));
retval = MulAdd(sum, sum, retval);
}
{ /* 0_________ 1______*__ 2_____*___ 3_____*___ 4____0____ 5___*_____ 6___*_____ 7__*______
8_________ */ auto sum = Sum(LoadU(df, d - xs3 + 2), LoadU(df, d - xs - xs + 1),
LoadU(df, d - xs + 1), center, LoadU(df, d + xs - 1),
LoadU(df, d + xs + xs - 1), LoadU(df, d + xs3 - 2));
retval = MulAdd(sum, sum, retval);
}
{ /* 0_________ 1_________ 2_*_______ 3__**_____ 4____0____ 5_____**__ 6_______*_ 7_________
8_________ */ auto sum = Sum(LoadU(df, d - xs - xs - 3), LoadU(df, d - xs - 2),
LoadU(df, d - xs - 1), center, LoadU(df, d + xs + 1),
LoadU(df, d + xs + 2), LoadU(df, d + xs + xs + 3));
retval = MulAdd(sum, sum, retval);
}
{ /* 0_________ 1_________ 2_______*_ 3_____**__ 4____0____ 5__**_____ 6_*_______ 7_________
8_________ */ auto sum = Sum(LoadU(df, d - xs - xs + 3), LoadU(df, d - xs + 2),
LoadU(df, d - xs + 1), center, LoadU(df, d + xs - 1),
LoadU(df, d + xs - 2), LoadU(df, d + xs + xs - 3));
retval = MulAdd(sum, sum, retval);
}
{ /* 0_________ 1_________ 2_________ 3______*** 4___*0*___ 5***______ 6_________ 7_________
8_________ */
auto sum =
Sum(LoadU(df, d + xs - 4), LoadU(df, d + xs - 3), LoadU(df, d + xs - 2),
LoadU(df, d - 1), center, LoadU(df, d + 1), LoadU(df, d - xs + 2),
LoadU(df, d - xs + 3), LoadU(df, d - xs + 4));
retval = MulAdd(sum, sum, retval);
}
{ /* 0_________ 1_________ 2_________ 3***______ 4___*0*___ 5______*** 6_________ 7_________
8_________ */ auto sum =
Sum(LoadU(df, d - xs - 4), LoadU(df, d - xs - 3), LoadU(df, d - xs - 2),
LoadU(df, d - 1), center, LoadU(df, d + 1), LoadU(df, d + xs + 2),
LoadU(df, d + xs + 3), LoadU(df, d + xs + 4));
retval = MulAdd(sum, sum, retval);
}
{ /* 0___*_____ 1___*_____ 2___*_____ 3____*____ 4____0____ 5____*____ 6_____*___ 7_____*___
8_____*___ */ auto sum = Sum(LoadU(df, d - xs3 - xs - 1), LoadU(df, d - xs3 - 1),
LoadU(df, d - xs - xs - 1), LoadU(df, d - xs), center,
LoadU(df, d + xs), LoadU(df, d + xs + xs + 1),
LoadU(df, d + xs3 + 1), LoadU(df, d + xs3 + xs + 1));
retval = MulAdd(sum, sum, retval);
}
{ /* 0_____*___ 1_____*___ 2____ *___ 3____*____ 4____0____ 5____*____ 6___*_____ 7___*_____
8___*_____ */ auto sum = Sum(LoadU(df, d - xs3 - xs + 1), LoadU(df, d - xs3 + 1),
LoadU(df, d - xs - xs + 1), LoadU(df, d - xs), center,
LoadU(df, d + xs), LoadU(df, d + xs + xs - 1),
LoadU(df, d + xs3 - 1), LoadU(df, d + xs3 + xs - 1));
retval = MulAdd(sum, sum, retval);
} return retval;
}
// Returns MaltaUnit. Avoids bounds-checks when x0 and y0 are known // to be far enough from the image borders. "diffs" is a packed image. template <class Tag> static BUTTERAUGLI_INLINE float PaddedMaltaUnit(const ImageF& diffs, const size_t x0, const size_t y0) { constfloat* BUTTERAUGLI_RESTRICT d = diffs.ConstRow(y0) + x0; const HWY_CAPPED(float, 1) df; if ((x0 >= 4 && y0 >= 4 && x0 < (diffs.xsize() - 4) &&
y0 < (diffs.ysize() - 4))) { return GetLane(MaltaUnit(Tag(), df, d, diffs.PixelsPerRow()));
}
float borderimage[12 * 9]; // round up to 4 for (int dy = 0; dy < 9; ++dy) { int y = y0 + dy - 4; if (y < 0 || static_cast<size_t>(y) >= diffs.ysize()) { for (int dx = 0; dx < 12; ++dx) {
borderimage[dy * 12 + dx] = 0.0f;
} continue;
}
// Initializes diffmap to the weighted L2 difference between i0 and i1. staticvoid SetL2Diff(const ImageF& i0, const ImageF& i1, constfloat w,
ImageF* BUTTERAUGLI_RESTRICT diffmap) { if (w == 0) return;
// A simple HDR compatible gamma function. template <class DF, class V>
V Gamma(const DF df, V v) { // ln(2) constant folded in because we want std::log but have FastLog2f. constauto kRetMul = Set(df, 19.245013259874995f * 0.693147180559945f); constauto kRetAdd = Set(df, -23.16046239805755); // This should happen rarely, but may lead to a NaN in log, which is // undesirable. Since negative photons don't exist we solve the NaNs by // clamping here.
v = ZeroIfNegative(v);
constauto biased = Add(v, Set(df, 9.9710635769299145)); constauto log = FastLog2f(df, biased); // We could fold this into a custom Log2 polynomial, but there would be // relatively little gain. return MulAdd(kRetMul, log, kRetAdd);
}
Status ButteraugliDiffmapInPlace(Image3F& image0, Image3F& image1, const ButteraugliParams& params,
ImageF& diffmap) { // image0 and image1 are in linear sRGB color space const size_t xsize = image0.xsize(); const size_t ysize = image0.ysize();
JxlMemoryManager* memory_manager = image0.memory_manager();
BlurTemp blur_temp;
{ // Convert image0 and image1 to XYB in-place
JXL_ASSIGN_OR_RETURN(Image3F temp,
Image3F::Create(memory_manager, xsize, ysize));
JXL_RETURN_IF_ERROR(
OpsinDynamicsImage(image0, params, &temp, &blur_temp, &image0));
JXL_RETURN_IF_ERROR(
OpsinDynamicsImage(image1, params, &temp, &blur_temp, &image1));
} // image0 and image1 are in XYB color space
JXL_ASSIGN_OR_RETURN(ImageF block_diff_dc,
ImageF::Create(memory_manager, xsize, ysize));
ZeroFillImage(&block_diff_dc);
{ // separate out LF components from image0 and image1 and compute the dc // diff image from them
JXL_ASSIGN_OR_RETURN(Image3F lf0,
Image3F::Create(memory_manager, xsize, ysize));
JXL_ASSIGN_OR_RETURN(Image3F lf1,
Image3F::Create(memory_manager, xsize, ysize));
JXL_RETURN_IF_ERROR(
SeparateLFAndMF(params, image0, &lf0, &image0, &blur_temp));
JXL_RETURN_IF_ERROR(
SeparateLFAndMF(params, image1, &lf1, &image1, &blur_temp)); for (size_t c = 0; c < 3; ++c) {
L2Diff(lf0.Plane(c), lf1.Plane(c), wmul[6 + c], &block_diff_dc);
}
} // image0 and image1 are MF residuals (before blurring) in XYB color space
ImageF hf0[2];
ImageF hf1[2];
JXL_RETURN_IF_ERROR(SeparateMFAndHF(params, &image0, &hf0[0], &blur_temp));
JXL_RETURN_IF_ERROR(SeparateMFAndHF(params, &image1, &hf1[0], &blur_temp)); // image0 and image1 are MF-images in XYB color space
JXL_ASSIGN_OR_RETURN(ImageF block_diff_ac,
ImageF::Create(memory_manager, xsize, ysize));
ZeroFillImage(&block_diff_ac); // start accumulating ac diff image from MF images
{
JXL_ASSIGN_OR_RETURN(ImageF diffs,
ImageF::Create(memory_manager, xsize, ysize));
JXL_RETURN_IF_ERROR(MaltaDiffMapLF(image0.Plane(1), image1.Plane(1),
wMfMalta, wMfMalta, norm1Mf, &diffs,
&block_diff_ac));
JXL_RETURN_IF_ERROR(MaltaDiffMapLF(image0.Plane(0), image1.Plane(0),
wMfMaltaX, wMfMaltaX, norm1MfX, &diffs,
&block_diff_ac));
} for (size_t c = 0; c < 3; ++c) {
L2Diff(image0.Plane(c), image1.Plane(c), wmul[3 + c], &block_diff_ac);
} // we will not need the MF-images and more, so we deallocate them to reduce // peak memory usage
image0 = Image3F();
image1 = Image3F();
HWY_EXPORT(SeparateFrequencies); // Local function.
HWY_EXPORT(MaskPsychoImage); // Local function.
HWY_EXPORT(L2DiffAsymmetric); // Local function.
HWY_EXPORT(L2Diff); // Local function.
HWY_EXPORT(SetL2Diff); // Local function.
HWY_EXPORT(CombineChannelsToDiffmap); // Local function.
HWY_EXPORT(MaltaDiffMap); // Local function.
HWY_EXPORT(MaltaDiffMapLF); // Local function.
HWY_EXPORT(OpsinDynamicsImage); // Local function.
HWY_EXPORT(ButteraugliDiffmapInPlace); // Local function.
staticinlinevoid CheckImage(const ImageF& image, constchar* name) { for (size_t y = 0; y < image.ysize(); ++y) { constfloat* BUTTERAUGLI_RESTRICT row = image.Row(y); for (size_t x = 0; x < image.xsize(); ++x) { if (IsNan(row[x])) {
printf("NAN: Image %s @ %" PRIuS ",%" PRIuS " (of %" PRIuS ",%" PRIuS ")\n",
name, x, y, image.xsize(), image.ysize()); exit(1);
}
}
}
}
#define CHECK_NAN(x, str) \ do { \ if (IsNan(x)) { \
printf("%d: %s\n", __LINE__, str); \
abort(); \
} \
} while (0)
// Calculate a 2x2 subsampled image for purposes of recursive butteraugli at // multiresolution. static StatusOr<Image3F> SubSample2x(const Image3F& in) {
size_t xs = (in.xsize() + 1) / 2;
size_t ys = (in.ysize() + 1) / 2;
JxlMemoryManager* memory_manager = in.memory_manager();
JXL_ASSIGN_OR_RETURN(Image3F retval, Image3F::Create(memory_manager, xs, ys)); for (size_t c = 0; c < 3; ++c) { for (size_t y = 0; y < ys; ++y) { for (size_t x = 0; x < xs; ++x) {
retval.PlaneRow(c, y)[x] = 0;
}
}
} for (size_t c = 0; c < 3; ++c) { for (size_t y = 0; y < in.ysize(); ++y) { for (size_t x = 0; x < in.xsize(); ++x) {
retval.PlaneRow(c, y / 2)[x / 2] += 0.25f * in.PlaneRow(c, y)[x];
}
} if ((in.xsize() & 1) != 0) { for (size_t y = 0; y < retval.ysize(); ++y) {
size_t last_column = retval.xsize() - 1;
retval.PlaneRow(c, y)[last_column] *= 2.0f;
}
} if ((in.ysize() & 1) != 0) { for (size_t x = 0; x < retval.xsize(); ++x) {
size_t last_row = retval.ysize() - 1;
retval.PlaneRow(c, last_row)[x] *= 2.0f;
}
}
} return retval;
}
// Supersample src by 2x and add it to dest. staticvoid AddSupersampled2x(const ImageF& src, float w, ImageF& dest) { for (size_t y = 0; y < dest.ysize(); ++y) { for (size_t x = 0; x < dest.xsize(); ++x) { // There will be less errors from the more averaged images. // We take it into account to some extent using a scaler. staticconstdouble kHeuristicMixingValue = 0.3;
dest.Row(y)[x] *= 1.0 - kHeuristicMixingValue * w;
dest.Row(y)[x] += w * src.Row(y / 2)[x / 2];
}
}
}
// Awful recursive construction of samples of different resolution. // This is an after-thought and possibly somewhat parallel in // functionality with the PsychoImage multi-resolution approach.
JXL_ASSIGN_OR_RETURN(Image3F subsampledRgb0, SubSample2x(rgb0));
JXL_ASSIGN_OR_RETURN(result->sub_,
ButteraugliComparator::Make(subsampledRgb0, params)); return result;
}
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.