/*------------------------------------------------------------------------- * drawElements Quality Program Tester Core * ---------------------------------------- * * Copyright 2014 The Android Open Source Project * * 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. * *//*! * \file * \brief Fuzzy image comparison. *//*--------------------------------------------------------------------*/ #include "tcuFuzzyImageCompare.hpp" #include "tcuTexture.hpp" #include "tcuTextureUtil.hpp" #include "deMath.h" #include "deRandom.hpp" #include namespace tcu { enum { MIN_ERR_THRESHOLD = 4 // Magic to make small differences go away }; using std::vector; template static inline deUint8 getChannel (deUint32 color) { return (deUint8)((color >> (Channel*8)) & 0xff); } static inline deUint8 getChannel (deUint32 color, int channel) { return (deUint8)((color >> (channel*8)) & 0xff); } static inline deUint32 setChannel (deUint32 color, int channel, deUint8 val) { return (color & ~(0xffu << (8*channel))) | (val << (8*channel)); } static inline Vec4 toFloatVec (deUint32 color) { return Vec4((float)getChannel<0>(color), (float)getChannel<1>(color), (float)getChannel<2>(color), (float)getChannel<3>(color)); } static inline deUint8 roundToUint8Sat (float v) { return (deUint8)de::clamp((int)(v + 0.5f), 0, 255); } static inline deUint32 toColor (Vec4 v) { return roundToUint8Sat(v[0]) | (roundToUint8Sat(v[1]) << 8) | (roundToUint8Sat(v[2]) << 16) | (roundToUint8Sat(v[3]) << 24); } template static inline deUint32 readUnorm8 (const tcu::ConstPixelBufferAccess& src, int x, int y) { const deUint8* ptr = (const deUint8*)src.getDataPtr() + src.getRowPitch()*y + x*NumChannels; deUint32 v = 0; for (int c = 0; c < NumChannels; c++) v |= ptr[c] << (c*8); if (NumChannels < 4) v |= 0xffu << 24; return v; } #if (DE_ENDIANNESS == DE_LITTLE_ENDIAN) template<> inline deUint32 readUnorm8<4> (const tcu::ConstPixelBufferAccess& src, int x, int y) { return *(const deUint32*)((const deUint8*)src.getDataPtr() + src.getRowPitch()*y + x*4); } #endif template static inline void writeUnorm8 (const tcu::PixelBufferAccess& dst, int x, int y, deUint32 val) { deUint8* ptr = (deUint8*)dst.getDataPtr() + dst.getRowPitch()*y + x*NumChannels; for (int c = 0; c < NumChannels; c++) ptr[c] = getChannel(val, c); } #if (DE_ENDIANNESS == DE_LITTLE_ENDIAN) template<> inline void writeUnorm8<4> (const tcu::PixelBufferAccess& dst, int x, int y, deUint32 val) { *(deUint32*)((deUint8*)dst.getDataPtr() + dst.getRowPitch()*y + x*4) = val; } #endif static inline deUint32 colorDistSquared (deUint32 pa, deUint32 pb) { const int r = de::max(de::abs((int)getChannel<0>(pa) - (int)getChannel<0>(pb)) - MIN_ERR_THRESHOLD, 0); const int g = de::max(de::abs((int)getChannel<1>(pa) - (int)getChannel<1>(pb)) - MIN_ERR_THRESHOLD, 0); const int b = de::max(de::abs((int)getChannel<2>(pa) - (int)getChannel<2>(pb)) - MIN_ERR_THRESHOLD, 0); const int a = de::max(de::abs((int)getChannel<3>(pa) - (int)getChannel<3>(pb)) - MIN_ERR_THRESHOLD, 0); return deUint32(r*r + g*g + b*b + a*a); } template inline deUint32 bilinearSample (const ConstPixelBufferAccess& src, float u, float v) { int w = src.getWidth(); int h = src.getHeight(); int x0 = deFloorFloatToInt32(u-0.5f); int x1 = x0+1; int y0 = deFloorFloatToInt32(v-0.5f); int y1 = y0+1; int i0 = de::clamp(x0, 0, w-1); int i1 = de::clamp(x1, 0, w-1); int j0 = de::clamp(y0, 0, h-1); int j1 = de::clamp(y1, 0, h-1); float a = deFloatFrac(u-0.5f); float b = deFloatFrac(v-0.5f); deUint32 p00 = readUnorm8(src, i0, j0); deUint32 p10 = readUnorm8(src, i1, j0); deUint32 p01 = readUnorm8(src, i0, j1); deUint32 p11 = readUnorm8(src, i1, j1); deUint32 dst = 0; // Interpolate. for (int c = 0; c < NumChannels; c++) { float f = (getChannel(p00, c)*(1.0f-a)*(1.0f-b)) + (getChannel(p10, c)*( a)*(1.0f-b)) + (getChannel(p01, c)*(1.0f-a)*( b)) + (getChannel(p11, c)*( a)*( b)); dst = setChannel(dst, c, roundToUint8Sat(f)); } return dst; } template static void separableConvolve (const PixelBufferAccess& dst, const ConstPixelBufferAccess& src, int shiftX, int shiftY, const std::vector& kernelX, const std::vector& kernelY) { DE_ASSERT(dst.getWidth() == src.getWidth() && dst.getHeight() == src.getHeight()); TextureLevel tmp (dst.getFormat(), dst.getHeight(), dst.getWidth()); PixelBufferAccess tmpAccess = tmp.getAccess(); int kw = (int)kernelX.size(); int kh = (int)kernelY.size(); // Horizontal pass // \note Temporary surface is written in column-wise order for (int j = 0; j < src.getHeight(); j++) { for (int i = 0; i < src.getWidth(); i++) { Vec4 sum(0); for (int kx = 0; kx < kw; kx++) { float f = kernelX[kw-kx-1]; deUint32 p = readUnorm8(src, de::clamp(i+kx-shiftX, 0, src.getWidth()-1), j); sum += toFloatVec(p)*f; } writeUnorm8(tmpAccess, j, i, toColor(sum)); } } // Vertical pass for (int j = 0; j < src.getHeight(); j++) { for (int i = 0; i < src.getWidth(); i++) { Vec4 sum(0.0f); for (int ky = 0; ky < kh; ky++) { float f = kernelY[kh-ky-1]; deUint32 p = readUnorm8(tmpAccess, de::clamp(j+ky-shiftY, 0, tmp.getWidth()-1), i); sum += toFloatVec(p)*f; } writeUnorm8(dst, i, j, toColor(sum)); } } } template static deUint32 distSquaredToNeighbor (de::Random& rnd, deUint32 pixel, const ConstPixelBufferAccess& surface, int x, int y) { // (x, y) + (0, 0) deUint32 minDist = colorDistSquared(pixel, readUnorm8(surface, x, y)); if (minDist == 0) return minDist; // Area around (x, y) static const int s_coords[][2] = { {-1, -1}, { 0, -1}, {+1, -1}, {-1, 0}, {+1, 0}, {-1, +1}, { 0, +1}, {+1, +1} }; for (int d = 0; d < (int)DE_LENGTH_OF_ARRAY(s_coords); d++) { int dx = x + s_coords[d][0]; int dy = y + s_coords[d][1]; if (!deInBounds32(dx, 0, surface.getWidth()) || !deInBounds32(dy, 0, surface.getHeight())) continue; minDist = de::min(minDist, colorDistSquared(pixel, readUnorm8(surface, dx, dy))); if (minDist == 0) return minDist; } // Random bilinear-interpolated samples around (x, y) for (int s = 0; s < 32; s++) { float dx = (float)x + rnd.getFloat()*2.0f - 0.5f; float dy = (float)y + rnd.getFloat()*2.0f - 0.5f; deUint32 sample = bilinearSample(surface, dx, dy); minDist = de::min(minDist, colorDistSquared(pixel, sample)); if (minDist == 0) return minDist; } return minDist; } static inline float toGrayscale (const Vec4& c) { return 0.2126f*c[0] + 0.7152f*c[1] + 0.0722f*c[2]; } static bool isFormatSupported (const TextureFormat& format) { return format.type == TextureFormat::UNORM_INT8 && (format.order == TextureFormat::RGB || format.order == TextureFormat::RGBA); } float fuzzyCompare (const FuzzyCompareParams& params, const ConstPixelBufferAccess& ref, const ConstPixelBufferAccess& cmp, const PixelBufferAccess& errorMask) { DE_ASSERT(ref.getWidth() == cmp.getWidth() && ref.getHeight() == cmp.getHeight()); DE_ASSERT(errorMask.getWidth() == ref.getWidth() && errorMask.getHeight() == ref.getHeight()); if (!isFormatSupported(ref.getFormat()) || !isFormatSupported(cmp.getFormat())) throw InternalError("Unsupported format in fuzzy comparison", DE_NULL, __FILE__, __LINE__); int width = ref.getWidth(); int height = ref.getHeight(); de::Random rnd (667); // Filtered TextureLevel refFiltered(TextureFormat(TextureFormat::RGBA, TextureFormat::UNORM_INT8), width, height); TextureLevel cmpFiltered(TextureFormat(TextureFormat::RGBA, TextureFormat::UNORM_INT8), width, height); // Kernel = {0.1, 0.8, 0.1} vector kernel(3); kernel[0] = kernel[2] = 0.1f; kernel[1]= 0.8f; int shift = (int)(kernel.size() - 1) / 2; switch (ref.getFormat().order) { case TextureFormat::RGBA: separableConvolve<4, 4>(refFiltered, ref, shift, shift, kernel, kernel); break; case TextureFormat::RGB: separableConvolve<4, 3>(refFiltered, ref, shift, shift, kernel, kernel); break; default: DE_ASSERT(DE_FALSE); } switch (cmp.getFormat().order) { case TextureFormat::RGBA: separableConvolve<4, 4>(cmpFiltered, cmp, shift, shift, kernel, kernel); break; case TextureFormat::RGB: separableConvolve<4, 3>(cmpFiltered, cmp, shift, shift, kernel, kernel); break; default: DE_ASSERT(DE_FALSE); } int numSamples = 0; deUint64 distSum4 = 0ull; // Clear error mask to green. clear(errorMask, Vec4(0.0f, 1.0f, 0.0f, 1.0f)); ConstPixelBufferAccess refAccess = refFiltered.getAccess(); ConstPixelBufferAccess cmpAccess = cmpFiltered.getAccess(); for (int y = 1; y < height-1; y++) { for (int x = 1; x < width-1; x += params.maxSampleSkip > 0 ? (int)rnd.getInt(1, params.maxSampleSkip) : 1) { const deUint32 minDist2RefToCmp = distSquaredToNeighbor<4>(rnd, readUnorm8<4>(refAccess, x, y), cmpAccess, x, y); const deUint32 minDist2CmpToRef = distSquaredToNeighbor<4>(rnd, readUnorm8<4>(cmpAccess, x, y), refAccess, x, y); const deUint32 minDist2 = de::min(minDist2RefToCmp, minDist2CmpToRef); const deUint64 newSum4 = distSum4 + minDist2*minDist2; distSum4 = (newSum4 >= distSum4) ? newSum4 : ~0ull; // In case of overflow numSamples += 1; // Build error image. { const int scale = 255-MIN_ERR_THRESHOLD; const float err2 = float(minDist2) / float(scale*scale); const float err4 = err2*err2; const float red = err4 * 500.0f; const float luma = toGrayscale(cmp.getPixel(x, y)); const float rF = 0.7f + 0.3f*luma; errorMask.setPixel(Vec4(red*rF, (1.0f-red)*rF, 0.0f, 1.0f), x, y); } } } { // Scale error sum based on number of samples taken const double pSamples = double((width-2) * (height-2)) / double(numSamples); const deUint64 colScale = deUint64(255-MIN_ERR_THRESHOLD); const deUint64 colScale4 = colScale*colScale*colScale*colScale; return float(double(distSum4) / double(colScale4) * pSamples); } } } // tcu