blob: 3563642880f08edd9662eba29dbb46ebf72fc38e [file] [log] [blame]
halcanary385fe4d2015-08-26 13:07:48 -07001/*
2 * Copyright 2013 Google Inc.
3 *
4 * Use of this source code is governed by a BSD-style license that can be
5 * found in the LICENSE file.
6 */
zachr@google.comc0a75a82013-06-28 15:34:56 +00007#include <cmath>
commit-bot@chromium.org89558c92014-05-21 11:57:12 +00008#include <math.h>
zachr@google.comc0a75a82013-06-28 15:34:56 +00009
10#include "SkBitmap.h"
11#include "skpdiff_util.h"
12#include "SkPMetric.h"
zachr@google.com92fe0732013-07-16 15:47:07 +000013#include "SkPMetricUtil_generated.h"
zachr@google.comc0a75a82013-06-28 15:34:56 +000014
15struct RGB {
16 float r, g, b;
17};
18
19struct LAB {
20 float l, a, b;
21};
22
23template<class T>
24struct Image2D {
25 int width;
26 int height;
27 T* image;
28
29 Image2D(int w, int h)
30 : width(w),
31 height(h) {
32 SkASSERT(w > 0);
33 SkASSERT(h > 0);
halcanary385fe4d2015-08-26 13:07:48 -070034 image = new T[w * h];
zachr@google.comc0a75a82013-06-28 15:34:56 +000035 }
36
halcanary385fe4d2015-08-26 13:07:48 -070037 ~Image2D() { delete[] image; }
zachr@google.comc0a75a82013-06-28 15:34:56 +000038
39 void readPixel(int x, int y, T* pixel) const {
40 SkASSERT(x >= 0);
41 SkASSERT(y >= 0);
42 SkASSERT(x < width);
43 SkASSERT(y < height);
44 *pixel = image[y * width + x];
45 }
46
zachr@google.coma79d40e2013-07-16 12:57:29 +000047 T* getRow(int y) const {
48 return &image[y * width];
49 }
50
zachr@google.comc0a75a82013-06-28 15:34:56 +000051 void writePixel(int x, int y, const T& pixel) {
52 SkASSERT(x >= 0);
53 SkASSERT(y >= 0);
54 SkASSERT(x < width);
55 SkASSERT(y < height);
56 image[y * width + x] = pixel;
57 }
58};
59
60typedef Image2D<float> ImageL;
61typedef Image2D<RGB> ImageRGB;
62typedef Image2D<LAB> ImageLAB;
63
64template<class T>
65struct ImageArray
66{
67 int slices;
68 Image2D<T>** image;
69
70 ImageArray(int w, int h, int s)
71 : slices(s) {
72 SkASSERT(s > 0);
halcanary385fe4d2015-08-26 13:07:48 -070073 image = new Image2D<T>* [s];
zachr@google.comc0a75a82013-06-28 15:34:56 +000074 for (int sliceIndex = 0; sliceIndex < slices; sliceIndex++) {
halcanary385fe4d2015-08-26 13:07:48 -070075 image[sliceIndex] = new Image2D<T>(w, h);
zachr@google.comc0a75a82013-06-28 15:34:56 +000076 }
77 }
78
79 ~ImageArray() {
80 for (int sliceIndex = 0; sliceIndex < slices; sliceIndex++) {
halcanary385fe4d2015-08-26 13:07:48 -070081 delete image[sliceIndex];
zachr@google.comc0a75a82013-06-28 15:34:56 +000082 }
halcanary385fe4d2015-08-26 13:07:48 -070083 delete[] image;
zachr@google.comc0a75a82013-06-28 15:34:56 +000084 }
85
86 Image2D<T>* getLayer(int z) const {
87 SkASSERT(z >= 0);
88 SkASSERT(z < slices);
89 return image[z];
90 }
91};
92
93typedef ImageArray<float> ImageL3D;
94
95
96#define MAT_ROW_MULT(rc,gc,bc) r*rc + g*gc + b*bc
97
zachr@google.com35f02fb2013-07-22 17:05:24 +000098static void adobergb_to_cielab(float r, float g, float b, LAB* lab) {
zachr@google.comc0a75a82013-06-28 15:34:56 +000099 // Conversion of Adobe RGB to XYZ taken from from "Adobe RGB (1998) ColorImage Encoding"
100 // URL:http://www.adobe.com/digitalimag/pdfs/AdobeRGB1998.pdf
101 // Section: 4.3.5.3
102 // See Also: http://en.wikipedia.org/wiki/Adobe_rgb
103 float x = MAT_ROW_MULT(0.57667f, 0.18556f, 0.18823f);
104 float y = MAT_ROW_MULT(0.29734f, 0.62736f, 0.07529f);
105 float z = MAT_ROW_MULT(0.02703f, 0.07069f, 0.99134f);
106
107 // The following is the white point in XYZ, so it's simply the row wise addition of the above
108 // matrix.
109 const float xw = 0.5767f + 0.185556f + 0.188212f;
110 const float yw = 0.297361f + 0.627355f + 0.0752847f;
111 const float zw = 0.0270328f + 0.0706879f + 0.991248f;
112
113 // This is the XYZ color point relative to the white point
114 float f[3] = { x / xw, y / yw, z / zw };
115
116 // Conversion from XYZ to LAB taken from
117 // http://en.wikipedia.org/wiki/CIELAB#Forward_transformation
118 for (int i = 0; i < 3; i++) {
119 if (f[i] >= 0.008856f) {
zachr@google.com92fe0732013-07-16 15:47:07 +0000120 f[i] = SkPMetricUtil::get_cube_root(f[i]);
zachr@google.comc0a75a82013-06-28 15:34:56 +0000121 } else {
122 f[i] = 7.787f * f[i] + 4.0f / 29.0f;
123 }
124 }
125 lab->l = 116.0f * f[1] - 16.0f;
126 lab->a = 500.0f * (f[0] - f[1]);
127 lab->b = 200.0f * (f[1] - f[2]);
128}
129
130/// Converts a 8888 bitmap to LAB color space and puts it into the output
scroggo@google.com086364b2013-11-12 14:41:20 +0000131static bool bitmap_to_cielab(const SkBitmap* bitmap, ImageLAB* outImageLAB) {
132 SkBitmap bm8888;
commit-bot@chromium.org28fcae22014-04-11 17:15:40 +0000133 if (bitmap->colorType() != kN32_SkColorType) {
134 if (!bitmap->copyTo(&bm8888, kN32_SkColorType)) {
scroggo@google.com086364b2013-11-12 14:41:20 +0000135 return false;
136 }
137 bitmap = &bm8888;
138 }
zachr@google.comc0a75a82013-06-28 15:34:56 +0000139
140 int width = bitmap->width();
141 int height = bitmap->height();
142 SkASSERT(outImageLAB->width == width);
143 SkASSERT(outImageLAB->height == height);
144
145 bitmap->lockPixels();
146 RGB rgb;
147 LAB lab;
148 for (int y = 0; y < height; y++) {
149 unsigned char* row = (unsigned char*)bitmap->getAddr(0, y);
150 for (int x = 0; x < width; x++) {
151 // Perform gamma correction which is assumed to be 2.2
zachr@google.com92fe0732013-07-16 15:47:07 +0000152 rgb.r = SkPMetricUtil::get_gamma(row[x * 4 + 2]);
153 rgb.g = SkPMetricUtil::get_gamma(row[x * 4 + 1]);
154 rgb.b = SkPMetricUtil::get_gamma(row[x * 4 + 0]);
zachr@google.comc0a75a82013-06-28 15:34:56 +0000155 adobergb_to_cielab(rgb.r, rgb.g, rgb.b, &lab);
156 outImageLAB->writePixel(x, y, lab);
157 }
158 }
159 bitmap->unlockPixels();
scroggo@google.com086364b2013-11-12 14:41:20 +0000160 return true;
zachr@google.comc0a75a82013-06-28 15:34:56 +0000161}
162
163// From Barten SPIE 1989
164static float contrast_sensitivity(float cyclesPerDegree, float luminance) {
165 float a = 440.0f * powf(1.0f + 0.7f / luminance, -0.2f);
zachr@google.com35f02fb2013-07-22 17:05:24 +0000166 float b = 0.3f * powf(1.0f + 100.0f / luminance, 0.15f);
commit-bot@chromium.org89558c92014-05-21 11:57:12 +0000167 float exp = expf(-b * cyclesPerDegree);
168 float root = sqrtf(1.0f + 0.06f * expf(b * cyclesPerDegree));
djsollen@google.com74ff1ba2014-05-21 12:24:58 +0000169 if (!SkScalarIsFinite(exp) || !SkScalarIsFinite(root)) {
commit-bot@chromium.org89558c92014-05-21 11:57:12 +0000170 return 0;
171 }
172 return a * cyclesPerDegree * exp * root;
zachr@google.comc0a75a82013-06-28 15:34:56 +0000173}
174
zachr@google.com92fe0732013-07-16 15:47:07 +0000175#if 0
176// We're keeping these around for reference and in case the lookup tables are no longer desired.
177// They are no longer called by any code in this file.
178
zachr@google.comc0a75a82013-06-28 15:34:56 +0000179// From Daly 1993
zachr@google.com92fe0732013-07-16 15:47:07 +0000180 static float visual_mask(float contrast) {
zachr@google.comc0a75a82013-06-28 15:34:56 +0000181 float x = powf(392.498f * contrast, 0.7f);
182 x = powf(0.0153f * x, 4.0f);
183 return powf(1.0f + x, 0.25f);
184}
185
186// From Ward Larson Siggraph 1997
187static float threshold_vs_intensity(float adaptationLuminance) {
188 float logLum = log10f(adaptationLuminance);
189 float x;
190 if (logLum < -3.94f) {
191 x = -2.86f;
192 } else if (logLum < -1.44f) {
193 x = powf(0.405f * logLum + 1.6f, 2.18) - 2.86f;
194 } else if (logLum < -0.0184f) {
195 x = logLum - 0.395f;
196 } else if (logLum < 1.9f) {
197 x = powf(0.249f * logLum + 0.65f, 2.7f) - 0.72f;
198 } else {
199 x = logLum - 1.255f;
200 }
201 return powf(10.0f, x);
202}
203
zachr@google.com92fe0732013-07-16 15:47:07 +0000204#endif
205
zachr@google.comc0a75a82013-06-28 15:34:56 +0000206/// Simply takes the L channel from the input and puts it into the output
207static void lab_to_l(const ImageLAB* imageLAB, ImageL* outImageL) {
208 for (int y = 0; y < imageLAB->height; y++) {
209 for (int x = 0; x < imageLAB->width; x++) {
210 LAB lab;
211 imageLAB->readPixel(x, y, &lab);
212 outImageL->writePixel(x, y, lab.l);
213 }
214 }
215}
216
217/// Convolves an image with the given filter in one direction and saves it to the output image
zachr@google.coma79d40e2013-07-16 12:57:29 +0000218static void convolve(const ImageL* imageL, bool vertical, ImageL* outImageL) {
zachr@google.comc0a75a82013-06-28 15:34:56 +0000219 SkASSERT(imageL->width == outImageL->width);
220 SkASSERT(imageL->height == outImageL->height);
zachr@google.coma79d40e2013-07-16 12:57:29 +0000221
222 const float matrix[] = { 0.05f, 0.25f, 0.4f, 0.25f, 0.05f };
223 const int matrixCount = sizeof(matrix) / sizeof(float);
224 const int radius = matrixCount / 2;
225
226 // Keep track of what rows are being operated on for quick access.
227 float* rowPtrs[matrixCount]; // Because matrixCount is constant, this won't create a VLA
228 for (int y = radius; y < matrixCount; y++) {
229 rowPtrs[y] = imageL->getRow(y - radius);
230 }
231 float* writeRow = outImageL->getRow(0);
232
zachr@google.comc0a75a82013-06-28 15:34:56 +0000233 for (int y = 0; y < imageL->height; y++) {
234 for (int x = 0; x < imageL->width; x++) {
235 float lSum = 0.0f;
zachr@google.comc0a75a82013-06-28 15:34:56 +0000236 for (int xx = -radius; xx <= radius; xx++) {
237 int nx = x;
238 int ny = y;
239
240 // We mirror at edges so that edge pixels that the filter weighting still makes
241 // sense.
242 if (vertical) {
243 ny += xx;
244 if (ny < 0) {
245 ny = -ny;
246 }
247 if (ny >= imageL->height) {
248 ny = imageL->height + (imageL->height - ny - 1);
249 }
250 } else {
251 nx += xx;
252 if (nx < 0) {
253 nx = -nx;
254 }
255 if (nx >= imageL->width) {
256 nx = imageL->width + (imageL->width - nx - 1);
257 }
258 }
259
zachr@google.comc0a75a82013-06-28 15:34:56 +0000260 float weight = matrix[xx + radius];
zachr@google.coma79d40e2013-07-16 12:57:29 +0000261 lSum += rowPtrs[ny - y + radius][nx] * weight;
zachr@google.comc0a75a82013-06-28 15:34:56 +0000262 }
zachr@google.coma79d40e2013-07-16 12:57:29 +0000263 writeRow[x] = lSum;
zachr@google.comc0a75a82013-06-28 15:34:56 +0000264 }
zachr@google.coma79d40e2013-07-16 12:57:29 +0000265 // As we move down, scroll the row pointers down with us
266 for (int y = 0; y < matrixCount - 1; y++)
267 {
268 rowPtrs[y] = rowPtrs[y + 1];
269 }
270 rowPtrs[matrixCount - 1] += imageL->width;
271 writeRow += imageL->width;
zachr@google.comc0a75a82013-06-28 15:34:56 +0000272 }
273}
274
djsollen@google.comefc51b72013-11-12 18:29:17 +0000275static double pmetric(const ImageLAB* baselineLAB, const ImageLAB* testLAB, int* poiCount) {
276 SkASSERT(baselineLAB);
277 SkASSERT(testLAB);
278 SkASSERT(poiCount);
279
zachr@google.comc0a75a82013-06-28 15:34:56 +0000280 int width = baselineLAB->width;
281 int height = baselineLAB->height;
zachr@google.com35f02fb2013-07-22 17:05:24 +0000282 int maxLevels = 0;
zachr@google.comc0a75a82013-06-28 15:34:56 +0000283
zachr@google.com35f02fb2013-07-22 17:05:24 +0000284 // Calculates how many levels to make by how many times the image can be divided in two
285 int smallerDimension = width < height ? width : height;
286 for ( ; smallerDimension > 1; smallerDimension /= 2) {
287 maxLevels++;
288 }
289
scroggo@google.com086364b2013-11-12 14:41:20 +0000290 // We'll be creating new arrays with maxLevels - 2, and ImageL3D requires maxLevels > 0,
291 // so just return failure if we're less than 3.
292 if (maxLevels <= 2) {
293 return 0.0;
294 }
295
zachr@google.com35f02fb2013-07-22 17:05:24 +0000296 const float fov = SK_ScalarPI / 180.0f * 45.0f;
zachr@google.comc0a75a82013-06-28 15:34:56 +0000297 float contrastSensitivityMax = contrast_sensitivity(3.248f, 100.0f);
zachr@google.com35f02fb2013-07-22 17:05:24 +0000298 float pixelsPerDegree = width / (2.0f * tanf(fov * 0.5f) * 180.0f / SK_ScalarPI);
zachr@google.comc0a75a82013-06-28 15:34:56 +0000299
300 ImageL3D baselineL(width, height, maxLevels);
301 ImageL3D testL(width, height, maxLevels);
302 ImageL scratchImageL(width, height);
halcanary385fe4d2015-08-26 13:07:48 -0700303 float* cyclesPerDegree = new float[maxLevels];
304 float* thresholdFactorFrequency = new float[maxLevels - 2];
305 float* contrast = new float[maxLevels - 2];
zachr@google.comc0a75a82013-06-28 15:34:56 +0000306
307 lab_to_l(baselineLAB, baselineL.getLayer(0));
308 lab_to_l(testLAB, testL.getLayer(0));
309
310 // Compute cpd - Cycles per degree on the pyramid
311 cyclesPerDegree[0] = 0.5f * pixelsPerDegree;
312 for (int levelIndex = 1; levelIndex < maxLevels; levelIndex++) {
313 cyclesPerDegree[levelIndex] = cyclesPerDegree[levelIndex - 1] * 0.5f;
314 }
315
zachr@google.com92fe0732013-07-16 15:47:07 +0000316 // Contrast sensitivity is based on image dimensions. Therefore it cannot be statically
317 // generated.
halcanary385fe4d2015-08-26 13:07:48 -0700318 float* contrastSensitivityTable = new float[maxLevels * 1000];
zachr@google.com92fe0732013-07-16 15:47:07 +0000319 for (int levelIndex = 0; levelIndex < maxLevels; levelIndex++) {
320 for (int csLum = 0; csLum < 1000; csLum++) {
321 contrastSensitivityTable[levelIndex * 1000 + csLum] =
322 contrast_sensitivity(cyclesPerDegree[levelIndex], (float)csLum / 10.0f + 1e-5f);
323 }
324 }
325
zachr@google.comc0a75a82013-06-28 15:34:56 +0000326 // Compute G - The convolved lum for the baseline
327 for (int levelIndex = 1; levelIndex < maxLevels; levelIndex++) {
zachr@google.coma79d40e2013-07-16 12:57:29 +0000328 convolve(baselineL.getLayer(levelIndex - 1), false, &scratchImageL);
329 convolve(&scratchImageL, true, baselineL.getLayer(levelIndex));
zachr@google.comc0a75a82013-06-28 15:34:56 +0000330 }
331 for (int levelIndex = 1; levelIndex < maxLevels; levelIndex++) {
zachr@google.coma79d40e2013-07-16 12:57:29 +0000332 convolve(testL.getLayer(levelIndex - 1), false, &scratchImageL);
333 convolve(&scratchImageL, true, testL.getLayer(levelIndex));
zachr@google.comc0a75a82013-06-28 15:34:56 +0000334 }
335
336 // Compute F_freq - The elevation f
337 for (int levelIndex = 0; levelIndex < maxLevels - 2; levelIndex++) {
338 float cpd = cyclesPerDegree[levelIndex];
339 thresholdFactorFrequency[levelIndex] = contrastSensitivityMax /
340 contrast_sensitivity(cpd, 100.0f);
341 }
342
zachr@google.comc0a75a82013-06-28 15:34:56 +0000343 // Calculate F
344 for (int y = 0; y < height; y++) {
345 for (int x = 0; x < width; x++) {
346 float lBaseline;
347 float lTest;
348 baselineL.getLayer(0)->readPixel(x, y, &lBaseline);
349 testL.getLayer(0)->readPixel(x, y, &lTest);
350
351 float avgLBaseline;
352 float avgLTest;
353 baselineL.getLayer(maxLevels - 1)->readPixel(x, y, &avgLBaseline);
354 testL.getLayer(maxLevels - 1)->readPixel(x, y, &avgLTest);
355
356 float lAdapt = 0.5f * (avgLBaseline + avgLTest);
zachr@google.com35f02fb2013-07-22 17:05:24 +0000357 if (lAdapt < 1e-5f) {
358 lAdapt = 1e-5f;
zachr@google.comc0a75a82013-06-28 15:34:56 +0000359 }
360
361 float contrastSum = 0.0f;
362 for (int levelIndex = 0; levelIndex < maxLevels - 2; levelIndex++) {
363 float baselineL0, baselineL1, baselineL2;
364 float testL0, testL1, testL2;
365 baselineL.getLayer(levelIndex + 0)->readPixel(x, y, &baselineL0);
366 testL. getLayer(levelIndex + 0)->readPixel(x, y, &testL0);
367 baselineL.getLayer(levelIndex + 1)->readPixel(x, y, &baselineL1);
368 testL. getLayer(levelIndex + 1)->readPixel(x, y, &testL1);
369 baselineL.getLayer(levelIndex + 2)->readPixel(x, y, &baselineL2);
370 testL. getLayer(levelIndex + 2)->readPixel(x, y, &testL2);
371
372 float baselineContrast1 = fabsf(baselineL0 - baselineL1);
373 float testContrast1 = fabsf(testL0 - testL1);
374 float numerator = (baselineContrast1 > testContrast1) ?
375 baselineContrast1 : testContrast1;
376
377 float baselineContrast2 = fabsf(baselineL2);
378 float testContrast2 = fabsf(testL2);
379 float denominator = (baselineContrast2 > testContrast2) ?
380 baselineContrast2 : testContrast2;
381
382 // Avoid divides by close to zero
zachr@google.com35f02fb2013-07-22 17:05:24 +0000383 if (denominator < 1e-5f) {
384 denominator = 1e-5f;
zachr@google.comc0a75a82013-06-28 15:34:56 +0000385 }
zachr@google.comc0a75a82013-06-28 15:34:56 +0000386 contrast[levelIndex] = numerator / denominator;
387 contrastSum += contrast[levelIndex];
388 }
389
zachr@google.com35f02fb2013-07-22 17:05:24 +0000390 if (contrastSum < 1e-5f) {
391 contrastSum = 1e-5f;
zachr@google.comc0a75a82013-06-28 15:34:56 +0000392 }
393
394 float F = 0.0f;
395 for (int levelIndex = 0; levelIndex < maxLevels - 2; levelIndex++) {
zachr@google.com92fe0732013-07-16 15:47:07 +0000396 float contrastSensitivity = contrastSensitivityTable[levelIndex * 1000 +
397 (int)(lAdapt * 10.0)];
398 float mask = SkPMetricUtil::get_visual_mask(contrast[levelIndex] *
399 contrastSensitivity);
zachr@google.comc0a75a82013-06-28 15:34:56 +0000400
401 F += contrast[levelIndex] +
402 thresholdFactorFrequency[levelIndex] * mask / contrastSum;
403 }
404
405 if (F < 1.0f) {
406 F = 1.0f;
407 }
408
409 if (F > 10.0f) {
410 F = 10.0f;
411 }
412
413
414 bool isFailure = false;
zachr@google.com92fe0732013-07-16 15:47:07 +0000415 if (fabsf(lBaseline - lTest) > F * SkPMetricUtil::get_threshold_vs_intensity(lAdapt)) {
zachr@google.comc0a75a82013-06-28 15:34:56 +0000416 isFailure = true;
417 } else {
418 LAB baselineColor;
419 LAB testColor;
420 baselineLAB->readPixel(x, y, &baselineColor);
421 testLAB->readPixel(x, y, &testColor);
422 float contrastA = baselineColor.a - testColor.a;
423 float contrastB = baselineColor.b - testColor.b;
424 float colorScale = 1.0f;
425 if (lAdapt < 10.0f) {
426 colorScale = lAdapt / 10.0f;
427 }
428 colorScale *= colorScale;
429
430 if ((contrastA * contrastA + contrastB * contrastB) * colorScale > F)
431 {
432 isFailure = true;
433 }
434 }
435
436 if (isFailure) {
djsollen@google.comefc51b72013-11-12 18:29:17 +0000437 (*poiCount)++;
zachr@google.comc0a75a82013-06-28 15:34:56 +0000438 }
439 }
440 }
441
halcanary385fe4d2015-08-26 13:07:48 -0700442 delete[] cyclesPerDegree;
443 delete[] contrast;
444 delete[] thresholdFactorFrequency;
445 delete[] contrastSensitivityTable;
djsollen@google.comefc51b72013-11-12 18:29:17 +0000446 return 1.0 - (double)(*poiCount) / (width * height);
zachr@google.comc0a75a82013-06-28 15:34:56 +0000447}
448
epoger54f1ad82014-07-02 07:43:04 -0700449bool SkPMetric::diff(SkBitmap* baseline, SkBitmap* test, const BitmapsToCreate& bitmapsToCreate,
450 Result* result) const {
zachr@google.comc0a75a82013-06-28 15:34:56 +0000451 double startTime = get_seconds();
zachr@google.comc0a75a82013-06-28 15:34:56 +0000452
453 // Ensure the images are comparable
454 if (baseline->width() != test->width() || baseline->height() != test->height() ||
455 baseline->width() <= 0 || baseline->height() <= 0) {
djsollen@google.comefc51b72013-11-12 18:29:17 +0000456 return false;
zachr@google.comc0a75a82013-06-28 15:34:56 +0000457 }
458
459 ImageLAB baselineLAB(baseline->width(), baseline->height());
460 ImageLAB testLAB(baseline->width(), baseline->height());
461
scroggo@google.com086364b2013-11-12 14:41:20 +0000462 if (!bitmap_to_cielab(baseline, &baselineLAB) || !bitmap_to_cielab(test, &testLAB)) {
djsollen@google.comefc51b72013-11-12 18:29:17 +0000463 return true;
scroggo@google.com086364b2013-11-12 14:41:20 +0000464 }
zachr@google.comc0a75a82013-06-28 15:34:56 +0000465
djsollen@google.comefc51b72013-11-12 18:29:17 +0000466 result->poiCount = 0;
467 result->result = pmetric(&baselineLAB, &testLAB, &result->poiCount);
468 result->timeElapsed = get_seconds() - startTime;
zachr@google.comc0a75a82013-06-28 15:34:56 +0000469
djsollen@google.comefc51b72013-11-12 18:29:17 +0000470 return true;
zachr@google.com572b54d2013-06-28 16:27:33 +0000471}