diff --git a/src/libOpenImageIO/maketexture.cpp b/src/libOpenImageIO/maketexture.cpp index 8e5d2edeb7..d7cbc4d8b1 100644 --- a/src/libOpenImageIO/maketexture.cpp +++ b/src/libOpenImageIO/maketexture.cpp @@ -1184,6 +1184,88 @@ make_texture_impl(ImageBufAlgo::MakeTextureMode mode, const ImageBuf* input, mode = ImageBufAlgo::MakeTxTexture; src = bumpslopes; } + + if (configspec.get_int_attribute("maketx:cdf")) { + // Writes Gaussian CDF and Inverse Gaussian CDF as per-channel + // metadata. We provide both the inverse transfrom and forward + // transfrom, so in theory we're free to change the distribution. + // + // References: + // + // Brent Burley, On Histogram-Preserving Blending for Randomized + // Texture Tiling, Journal of Computer Graphics Techniques (JCGT), + // vol. 8, no. 4, 31-53, 2019 + // + // Eric Heitz and Fabrice Neyret, High-Performance By-Example Noise + // using a Histogram-Preserving Blending Operator, + // https://hal.inria.fr/hal-01824773}, Proceedings of the ACM on + // Computer Graphics and Interactive Techniques, ACM SIGGRAPH / + // Eurographics Symposium on High-Performance Graphics 2018, + // + // Benedikt Bitterli + // https://benedikt-bitterli.me/histogram-tiling/ + + const float cdf_sigma = configspec.get_float_attribute( + "maketx:cdfsigma"); + const int cdf_bits = configspec.get_int_attribute("maketx:cdfbits"); + const uint64_t bins = 1 << cdf_bits; + + // Normalization coefficient for the truncated normal distribution + const float c_sigma_inv = fast_erf(1.0f / (2.0f * M_SQRT2 * cdf_sigma)); + + // If there are channels other than R,G,B,A, we probably shouldn't do + // anything to them. + const int channels = std::min(4, src->spec().nchannels); + + std::vector invCDF(bins); + std::vector CDF(bins); + std::vector hist; + + for (int i = 0; i < channels; i++) { + hist = ImageBufAlgo::histogram(*src, i, bins, 0.0f, 1.0f); + + // Turn the histogram into a non-normalized CDF + for (uint64_t j = 1; j < bins; j++) { + hist[j] += hist[j - 1]; + } + + // Store the inverse CDF as a lookup-table which we'll use to + // transform the image data to a Guassian distribution. As + // mentioned in Burley [2019] we're combining two steps here when + // using the invCDF lookup table: we first "look up" the image + // value through its CDF (the normalized histogram) which gives us + // a uniformly distributed value, which we're then feeding in to + // the Gaussian inverse CDF to transfrom the unifrom distribution + // to Gaussian. + for (uint64_t j = 0; j < bins; j++) { + float u = float(hist[j]) / hist[bins - 1]; + float g = 0.5f + + cdf_sigma * M_SQRT2 + * fast_ierf(c_sigma_inv * (2.0f * u - 1.0f)); + invCDF[j] = std::min(1.0f, std::max(0.0f, g)); + } + configspec.attribute("invCDF_" + std::to_string(i), + TypeDesc(TypeDesc::FLOAT, bins), + invCDF.data()); + + // Store the forward CDF as a lookup table to transform back to + // the original image distribution from a Gaussian distribution. + for (uint64_t j = 0; j < bins; j++) { + auto upper = std::upper_bound(invCDF.begin(), invCDF.end(), + float(j) / (float(bins - 1))); + CDF[j] = clamp(float(upper - invCDF.begin()) / float(bins - 1), + 0.0f, 1.0f); + } + + configspec.attribute("CDF_" + std::to_string(i), + TypeDesc(TypeDesc::FLOAT, bins), CDF.data()); + } + + configspec["CDF_bits"] = cdf_bits; + + mode = ImageBufAlgo::MakeTxTexture; + } + double misc_time_2 = alltime.lap(); STATUS("misc2", misc_time_2); diff --git a/src/maketx/maketx.cpp b/src/maketx/maketx.cpp index 17ff28dbb5..f67f06c442 100644 --- a/src/maketx/maketx.cpp +++ b/src/maketx/maketx.cpp @@ -177,6 +177,9 @@ getargs(int argc, char* argv[], ImageSpec& configspec) bool sansattrib = false; float sharpen = 0.0f; float uvslopes_scale = 0.0f; + bool cdf = false; + float cdfsigma = 1.0f / 6; + int cdfbits = 8; std::string incolorspace; std::string outcolorspace; std::string colorconfigname; @@ -285,6 +288,13 @@ getargs(int argc, char* argv[], ImageSpec& configspec) .hidden(); // DEPRECATED 1.6 ap.arg("--mipimage %L:FILENAME", &mipimages) .help("Specify an individual MIP level"); + ap.arg("--cdf %d:N", &cdf) + .help("Store the forward and inverse Gaussian CDF as a lookup-table. The variance is set by cdfsigma (1/6 by default), and the number of buckets \ + in the lookup table is determined by cdfbits (8 bit - 256 buckets by default)"); + ap.arg("--cdfsigma %f:N", &cdfsigma) + .help("Specify the Gaussian sigma parameter when writing the forward and inverse Gaussian CDF data. The default vale is 1/6 (0.1667)"); + ap.arg("--cdfbits %d:N", &cdfbits) + .help("Specify the number of bits used to store the forward and inverse Guassian CDF. The default value is 8 bits"); ap.separator("Basic modes (default is plain texture):"); ap.arg("--shadow", &shadowmode) @@ -426,6 +436,9 @@ getargs(int argc, char* argv[], ImageSpec& configspec) configspec.attribute("maketx:mipimages", Strutil::join(mipimages, ";")); if (bumpslopesmode) configspec.attribute("maketx:bumpformat", bumpformat); + configspec.attribute("maketx:cdf", cdf); + configspec.attribute("maketx:cdfsigma", cdfsigma); + configspec.attribute("maketx:cdfbits", cdfbits); std::string cmdline = Strutil::sprintf("OpenImageIO %s : %s", OIIO_VERSION_STRING, diff --git a/src/oiiotool/oiiotool.cpp b/src/oiiotool/oiiotool.cpp index 0e7e4d82af..664c374bbe 100644 --- a/src/oiiotool/oiiotool.cpp +++ b/src/oiiotool/oiiotool.cpp @@ -4692,6 +4692,14 @@ prep_texture_config(ImageSpec& configspec, ParamValueList& fileoptions) fileoptions.get_string("bumpformat", "auto")); configspec.attribute("maketx:uvslopes_scale", fileoptions.get_float("uvslopes_scale", 0.0f)); + + // The default values here should match the initialized values + // in src/maketx/maketx.cpp + configspec.attribute("maketx:cdf", fileoptions.get_int("cdf")); + configspec.attribute("maketx:cdfbits", fileoptions.get_int("cdfbits", 8)); + configspec.attribute("maketx:cdfsigma", + fileoptions.get_float("cdfsigma", 1.0f / 6)); + // if (mipimages.size()) // configspec.attribute ("maketx:mipimages", Strutil::join(mipimages,";"));