1/*-------------------------------------------------------------------------
2 * drawElements Quality Program Tester Core
3 * ----------------------------------------
4 *
5 * Copyright 2014 The Android Open Source Project
6 *
7 * Licensed under the Apache License, Version 2.0 (the "License");
8 * you may not use this file except in compliance with the License.
9 * You may obtain a copy of the License at
10 *
11 *      http://www.apache.org/licenses/LICENSE-2.0
12 *
13 * Unless required by applicable law or agreed to in writing, software
14 * distributed under the License is distributed on an "AS IS" BASIS,
15 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 * See the License for the specific language governing permissions and
17 * limitations under the License.
18 *
19 *//*!
20 * \file
21 * \brief Fuzzy image comparison.
22 *//*--------------------------------------------------------------------*/
23
24#include "tcuFuzzyImageCompare.hpp"
25#include "tcuTexture.hpp"
26#include "tcuTextureUtil.hpp"
27#include "deMath.h"
28#include "deRandom.hpp"
29
30#include <vector>
31
32namespace tcu
33{
34
35enum
36{
37	MIN_ERR_THRESHOLD	= 4 // Magic to make small differences go away
38};
39
40using std::vector;
41
42template<int Channel>
43static inline deUint8 getChannel (deUint32 color)
44{
45	return (deUint8)((color >> (Channel*8)) & 0xff);
46}
47
48static inline deUint8 getChannel (deUint32 color, int channel)
49{
50	return (deUint8)((color >> (channel*8)) & 0xff);
51}
52
53static inline deUint32 setChannel (deUint32 color, int channel, deUint8 val)
54{
55	return (color & ~(0xffu << (8*channel))) | (val << (8*channel));
56}
57
58static inline Vec4 toFloatVec (deUint32 color)
59{
60	return Vec4((float)getChannel<0>(color), (float)getChannel<1>(color), (float)getChannel<2>(color), (float)getChannel<3>(color));
61}
62
63static inline deUint8 roundToUint8Sat (float v)
64{
65	return (deUint8)de::clamp((int)(v + 0.5f), 0, 255);
66}
67
68static inline deUint32 toColor (Vec4 v)
69{
70	return roundToUint8Sat(v[0]) | (roundToUint8Sat(v[1]) << 8) | (roundToUint8Sat(v[2]) << 16) | (roundToUint8Sat(v[3]) << 24);
71}
72
73template<int NumChannels>
74static inline deUint32 readUnorm8 (const tcu::ConstPixelBufferAccess& src, int x, int y)
75{
76	const deUint8*	ptr	= (const deUint8*)src.getDataPtr() + src.getRowPitch()*y + x*NumChannels;
77	deUint32		v	= 0;
78
79	for (int c = 0; c < NumChannels; c++)
80		v |= ptr[c] << (c*8);
81
82	if (NumChannels < 4)
83		v |= 0xffu << 24;
84
85	return v;
86}
87
88#if (DE_ENDIANNESS == DE_LITTLE_ENDIAN)
89template<>
90inline deUint32 readUnorm8<4> (const tcu::ConstPixelBufferAccess& src, int x, int y)
91{
92	return *(const deUint32*)((const deUint8*)src.getDataPtr() + src.getRowPitch()*y + x*4);
93}
94#endif
95
96template<int NumChannels>
97static inline void writeUnorm8 (const tcu::PixelBufferAccess& dst, int x, int y, deUint32 val)
98{
99	deUint8* ptr = (deUint8*)dst.getDataPtr() + dst.getRowPitch()*y + x*NumChannels;
100
101	for (int c = 0; c < NumChannels; c++)
102		ptr[c] = getChannel(val, c);
103}
104
105#if (DE_ENDIANNESS == DE_LITTLE_ENDIAN)
106template<>
107inline void writeUnorm8<4> (const tcu::PixelBufferAccess& dst, int x, int y, deUint32 val)
108{
109	*(deUint32*)((deUint8*)dst.getDataPtr() + dst.getRowPitch()*y + x*4) = val;
110}
111#endif
112
113static inline deUint32 colorDistSquared (deUint32 pa, deUint32 pb)
114{
115	const int	r	= de::max<int>(de::abs((int)getChannel<0>(pa) - (int)getChannel<0>(pb)) - MIN_ERR_THRESHOLD, 0);
116	const int	g	= de::max<int>(de::abs((int)getChannel<1>(pa) - (int)getChannel<1>(pb)) - MIN_ERR_THRESHOLD, 0);
117	const int	b	= de::max<int>(de::abs((int)getChannel<2>(pa) - (int)getChannel<2>(pb)) - MIN_ERR_THRESHOLD, 0);
118	const int	a	= de::max<int>(de::abs((int)getChannel<3>(pa) - (int)getChannel<3>(pb)) - MIN_ERR_THRESHOLD, 0);
119
120	return deUint32(r*r + g*g + b*b + a*a);
121}
122
123template<int NumChannels>
124inline deUint32 bilinearSample (const ConstPixelBufferAccess& src, float u, float v)
125{
126	int w = src.getWidth();
127	int h = src.getHeight();
128
129	int x0 = deFloorFloatToInt32(u-0.5f);
130	int x1 = x0+1;
131	int y0 = deFloorFloatToInt32(v-0.5f);
132	int y1 = y0+1;
133
134	int i0 = de::clamp(x0, 0, w-1);
135	int i1 = de::clamp(x1, 0, w-1);
136	int j0 = de::clamp(y0, 0, h-1);
137	int j1 = de::clamp(y1, 0, h-1);
138
139	float a = deFloatFrac(u-0.5f);
140	float b = deFloatFrac(v-0.5f);
141
142	deUint32 p00	= readUnorm8<NumChannels>(src, i0, j0);
143	deUint32 p10	= readUnorm8<NumChannels>(src, i1, j0);
144	deUint32 p01	= readUnorm8<NumChannels>(src, i0, j1);
145	deUint32 p11	= readUnorm8<NumChannels>(src, i1, j1);
146	deUint32 dst	= 0;
147
148	// Interpolate.
149	for (int c = 0; c < NumChannels; c++)
150	{
151		float f = (getChannel(p00, c)*(1.0f-a)*(1.0f-b)) +
152				  (getChannel(p10, c)*(     a)*(1.0f-b)) +
153				  (getChannel(p01, c)*(1.0f-a)*(     b)) +
154				  (getChannel(p11, c)*(     a)*(     b));
155		dst = setChannel(dst, c, roundToUint8Sat(f));
156	}
157
158	return dst;
159}
160
161template<int DstChannels, int SrcChannels>
162static void separableConvolve (const PixelBufferAccess& dst, const ConstPixelBufferAccess& src, int shiftX, int shiftY, const std::vector<float>& kernelX, const std::vector<float>& kernelY)
163{
164	DE_ASSERT(dst.getWidth() == src.getWidth() && dst.getHeight() == src.getHeight());
165
166	TextureLevel		tmp			(dst.getFormat(), dst.getHeight(), dst.getWidth());
167	PixelBufferAccess	tmpAccess	= tmp.getAccess();
168
169	int kw = (int)kernelX.size();
170	int kh = (int)kernelY.size();
171
172	// Horizontal pass
173	// \note Temporary surface is written in column-wise order
174	for (int j = 0; j < src.getHeight(); j++)
175	{
176		for (int i = 0; i < src.getWidth(); i++)
177		{
178			Vec4 sum(0);
179
180			for (int kx = 0; kx < kw; kx++)
181			{
182				float		f = kernelX[kw-kx-1];
183				deUint32	p = readUnorm8<SrcChannels>(src, de::clamp(i+kx-shiftX, 0, src.getWidth()-1), j);
184
185				sum += toFloatVec(p)*f;
186			}
187
188			writeUnorm8<DstChannels>(tmpAccess, j, i, toColor(sum));
189		}
190	}
191
192	// Vertical pass
193	for (int j = 0; j < src.getHeight(); j++)
194	{
195		for (int i = 0; i < src.getWidth(); i++)
196		{
197			Vec4 sum(0.0f);
198
199			for (int ky = 0; ky < kh; ky++)
200			{
201				float		f = kernelY[kh-ky-1];
202				deUint32	p = readUnorm8<DstChannels>(tmpAccess, de::clamp(j+ky-shiftY, 0, tmp.getWidth()-1), i);
203
204				sum += toFloatVec(p)*f;
205			}
206
207			writeUnorm8<DstChannels>(dst, i, j, toColor(sum));
208		}
209	}
210}
211
212template<int NumChannels>
213static deUint32 distSquaredToNeighbor (de::Random& rnd, deUint32 pixel, const ConstPixelBufferAccess& surface, int x, int y)
214{
215	// (x, y) + (0, 0)
216	deUint32	minDist		= colorDistSquared(pixel, readUnorm8<NumChannels>(surface, x, y));
217
218	if (minDist == 0)
219		return minDist;
220
221	// Area around (x, y)
222	static const int s_coords[8][2] =
223	{
224		{-1, -1},
225		{ 0, -1},
226		{+1, -1},
227		{-1,  0},
228		{+1,  0},
229		{-1, +1},
230		{ 0, +1},
231		{+1, +1}
232	};
233
234	for (int d = 0; d < (int)DE_LENGTH_OF_ARRAY(s_coords); d++)
235	{
236		int dx = x + s_coords[d][0];
237		int dy = y + s_coords[d][1];
238
239		if (!deInBounds32(dx, 0, surface.getWidth()) || !deInBounds32(dy, 0, surface.getHeight()))
240			continue;
241
242		minDist = de::min(minDist, colorDistSquared(pixel, readUnorm8<NumChannels>(surface, dx, dy)));
243		if (minDist == 0)
244			return minDist;
245	}
246
247	// Random bilinear-interpolated samples around (x, y)
248	for (int s = 0; s < 32; s++)
249	{
250		float dx = (float)x + rnd.getFloat()*2.0f - 0.5f;
251		float dy = (float)y + rnd.getFloat()*2.0f - 0.5f;
252
253		deUint32 sample = bilinearSample<NumChannels>(surface, dx, dy);
254
255		minDist = de::min(minDist, colorDistSquared(pixel, sample));
256		if (minDist == 0)
257			return minDist;
258	}
259
260	return minDist;
261}
262
263static inline float toGrayscale (const Vec4& c)
264{
265	return 0.2126f*c[0] + 0.7152f*c[1] + 0.0722f*c[2];
266}
267
268static bool isFormatSupported (const TextureFormat& format)
269{
270	return format.type == TextureFormat::UNORM_INT8 && (format.order == TextureFormat::RGB || format.order == TextureFormat::RGBA);
271}
272
273float fuzzyCompare (const FuzzyCompareParams& params, const ConstPixelBufferAccess& ref, const ConstPixelBufferAccess& cmp, const PixelBufferAccess& errorMask)
274{
275	DE_ASSERT(ref.getWidth() == cmp.getWidth() && ref.getHeight() == cmp.getHeight());
276	DE_ASSERT(errorMask.getWidth() == ref.getWidth() && errorMask.getHeight() == ref.getHeight());
277
278	if (!isFormatSupported(ref.getFormat()) || !isFormatSupported(cmp.getFormat()))
279		throw InternalError("Unsupported format in fuzzy comparison", DE_NULL, __FILE__, __LINE__);
280
281	int			width	= ref.getWidth();
282	int			height	= ref.getHeight();
283	de::Random	rnd		(667);
284
285	// Filtered
286	TextureLevel refFiltered(TextureFormat(TextureFormat::RGBA, TextureFormat::UNORM_INT8), width, height);
287	TextureLevel cmpFiltered(TextureFormat(TextureFormat::RGBA, TextureFormat::UNORM_INT8), width, height);
288
289	// Kernel = {0.1, 0.8, 0.1}
290	vector<float> kernel(3);
291	kernel[0] = kernel[2] = 0.1f; kernel[1]= 0.8f;
292	int shift = (int)(kernel.size() - 1) / 2;
293
294	switch (ref.getFormat().order)
295	{
296		case TextureFormat::RGBA:	separableConvolve<4, 4>(refFiltered, ref, shift, shift, kernel, kernel);	break;
297		case TextureFormat::RGB:	separableConvolve<4, 3>(refFiltered, ref, shift, shift, kernel, kernel);	break;
298		default:
299			DE_ASSERT(DE_FALSE);
300	}
301
302	switch (cmp.getFormat().order)
303	{
304		case TextureFormat::RGBA:	separableConvolve<4, 4>(cmpFiltered, cmp, shift, shift, kernel, kernel);	break;
305		case TextureFormat::RGB:	separableConvolve<4, 3>(cmpFiltered, cmp, shift, shift, kernel, kernel);	break;
306		default:
307			DE_ASSERT(DE_FALSE);
308	}
309
310	int			numSamples	= 0;
311	deUint64	distSum4	= 0ull;
312
313	// Clear error mask to green.
314	clear(errorMask, Vec4(0.0f, 1.0f, 0.0f, 1.0f));
315
316	ConstPixelBufferAccess refAccess = refFiltered.getAccess();
317	ConstPixelBufferAccess cmpAccess = cmpFiltered.getAccess();
318
319	for (int y = 1; y < height-1; y++)
320	{
321		for (int x = 1; x < width-1; x += params.maxSampleSkip > 0 ? (int)rnd.getInt(1, params.maxSampleSkip) : 1)
322		{
323			const deUint32	minDist2RefToCmp	= distSquaredToNeighbor<4>(rnd, readUnorm8<4>(refAccess, x, y), cmpAccess, x, y);
324			const deUint32	minDist2CmpToRef	= distSquaredToNeighbor<4>(rnd, readUnorm8<4>(cmpAccess, x, y), refAccess, x, y);
325			const deUint32	minDist2			= de::min(minDist2RefToCmp, minDist2CmpToRef);
326			const deUint64	newSum4				= distSum4 + minDist2*minDist2;
327
328			distSum4	 = (newSum4 >= distSum4) ? newSum4 : ~0ull; // In case of overflow
329			numSamples	+= 1;
330
331			// Build error image.
332			{
333				const int	scale	= 255-MIN_ERR_THRESHOLD;
334				const float	err2	= float(minDist2) / float(scale*scale);
335				const float	err4	= err2*err2;
336				const float	red		= err4 * 500.0f;
337				const float	luma	= toGrayscale(cmp.getPixel(x, y));
338				const float	rF		= 0.7f + 0.3f*luma;
339
340				errorMask.setPixel(Vec4(red*rF, (1.0f-red)*rF, 0.0f, 1.0f), x, y);
341			}
342		}
343	}
344
345	{
346		// Scale error sum based on number of samples taken
347		const double	pSamples	= double((width-2) * (height-2)) / double(numSamples);
348		const deUint64	colScale	= deUint64(255-MIN_ERR_THRESHOLD);
349		const deUint64	colScale4	= colScale*colScale*colScale*colScale;
350
351		return float(double(distSum4) / double(colScale4) * pSamples);
352	}
353}
354
355} // tcu
356