1 /*
2 * Copyright (C) 2018 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 #define LOG_NDEBUG 0
18 #define LOG_TAG "DistortionMapperTest"
19
20 #include <random>
21
22 #include <gtest/gtest.h>
23 #include <android-base/stringprintf.h>
24 #include <android-base/chrono_utils.h>
25
26 #include "../device3/DistortionMapper.h"
27
28 using namespace android;
29 using namespace android::camera3;
30
31
32 int32_t testActiveArray[] = {100, 100, 1000, 750};
33
34 float testICal[] = { 1000.f, 1000.f, 500.f, 500.f, 0.f };
35
36 float identityDistortion[] = { 0.f, 0.f, 0.f, 0.f, 0.f};
37
38 std::array<int32_t, 12> basicCoords = {
39 0, 0,
40 testActiveArray[2] - 1, 0,
41 testActiveArray[2] - 1, testActiveArray[3] - 1,
42 0, testActiveArray[3] - 1,
43 testActiveArray[2] / 2, testActiveArray[3] / 2,
44 251, 403 // A particularly bad coordinate for current grid count/array size
45 };
46
47
setupTestMapper(DistortionMapper * m,float distortion[5])48 void setupTestMapper(DistortionMapper *m, float distortion[5]) {
49 CameraMetadata deviceInfo;
50
51 deviceInfo.update(ANDROID_SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE,
52 testActiveArray, 4);
53
54 deviceInfo.update(ANDROID_LENS_INTRINSIC_CALIBRATION,
55 testICal, 5);
56
57 deviceInfo.update(ANDROID_LENS_DISTORTION,
58 distortion, 5);
59
60 m->setupStaticInfo(deviceInfo);
61 }
62
TEST(DistortionMapperTest,Initialization)63 TEST(DistortionMapperTest, Initialization) {
64 CameraMetadata deviceInfo;
65
66 ASSERT_FALSE(DistortionMapper::isDistortionSupported(deviceInfo));
67
68 uint8_t distortionModes[] =
69 {ANDROID_DISTORTION_CORRECTION_MODE_OFF,
70 ANDROID_DISTORTION_CORRECTION_MODE_FAST,
71 ANDROID_DISTORTION_CORRECTION_MODE_HIGH_QUALITY};
72
73 deviceInfo.update(ANDROID_DISTORTION_CORRECTION_AVAILABLE_MODES,
74 distortionModes, 1);
75
76 ASSERT_FALSE(DistortionMapper::isDistortionSupported(deviceInfo));
77
78 deviceInfo.update(ANDROID_DISTORTION_CORRECTION_AVAILABLE_MODES,
79 distortionModes, 3);
80
81 ASSERT_TRUE(DistortionMapper::isDistortionSupported(deviceInfo));
82
83 DistortionMapper m;
84
85 ASSERT_FALSE(m.calibrationValid());
86
87 ASSERT_NE(m.setupStaticInfo(deviceInfo), OK);
88
89 ASSERT_FALSE(m.calibrationValid());
90
91 deviceInfo.update(ANDROID_SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE,
92 testActiveArray, 4);
93
94 deviceInfo.update(ANDROID_LENS_INTRINSIC_CALIBRATION,
95 testICal, 5);
96
97 deviceInfo.update(ANDROID_LENS_DISTORTION,
98 identityDistortion, 5);
99
100 ASSERT_EQ(m.setupStaticInfo(deviceInfo), OK);
101
102 ASSERT_TRUE(m.calibrationValid());
103
104 CameraMetadata captureResult;
105
106 ASSERT_NE(m.updateCalibration(captureResult), OK);
107
108 captureResult.update(ANDROID_LENS_INTRINSIC_CALIBRATION,
109 testICal, 5);
110 captureResult.update(ANDROID_LENS_DISTORTION,
111 identityDistortion, 5);
112
113 ASSERT_EQ(m.updateCalibration(captureResult), OK);
114
115 }
116
TEST(DistortionMapperTest,IdentityTransform)117 TEST(DistortionMapperTest, IdentityTransform) {
118 status_t res;
119
120 DistortionMapper m;
121 setupTestMapper(&m, identityDistortion);
122
123 auto coords = basicCoords;
124 res = m.mapCorrectedToRaw(coords.data(), 5);
125 ASSERT_EQ(res, OK);
126
127 for (size_t i = 0; i < coords.size(); i++) {
128 EXPECT_EQ(coords[i], basicCoords[i]);
129 }
130
131 res = m.mapRawToCorrected(coords.data(), 5);
132 ASSERT_EQ(res, OK);
133
134 for (size_t i = 0; i < coords.size(); i++) {
135 EXPECT_EQ(coords[i], basicCoords[i]);
136 }
137
138 std::array<int32_t, 8> rects = {
139 0, 0, 100, 100,
140 testActiveArray[2] - 100, testActiveArray[3]-100, 100, 100
141 };
142
143 auto rectsOrig = rects;
144 res = m.mapCorrectedRectToRaw(rects.data(), 2);
145 ASSERT_EQ(res, OK);
146
147 for (size_t i = 0; i < rects.size(); i++) {
148 EXPECT_EQ(rects[i], rectsOrig[i]);
149 }
150
151 res = m.mapRawRectToCorrected(rects.data(), 2);
152 ASSERT_EQ(res, OK);
153
154 for (size_t i = 0; i < rects.size(); i++) {
155 EXPECT_EQ(rects[i], rectsOrig[i]);
156 }
157 }
158
TEST(DistortionMapperTest,LargeTransform)159 TEST(DistortionMapperTest, LargeTransform) {
160 status_t res;
161 constexpr int maxAllowedPixelError = 2; // Maximum per-pixel error allowed
162 constexpr int bucketsPerPixel = 3; // Histogram granularity
163
164 unsigned int seed = 1234; // Ensure repeatability for debugging
165 const size_t coordCount = 1e6; // Number of random test points
166
167 float bigDistortion[] = {0.1, -0.003, 0.004, 0.02, 0.01};
168
169 DistortionMapper m;
170 setupTestMapper(&m, bigDistortion);
171
172 std::default_random_engine gen(seed);
173
174 std::uniform_int_distribution<int> x_dist(0, testActiveArray[2] - 1);
175 std::uniform_int_distribution<int> y_dist(0, testActiveArray[3] - 1);
176
177 std::vector<int32_t> randCoords(coordCount * 2);
178
179 for (size_t i = 0; i < randCoords.size(); i += 2) {
180 randCoords[i] = x_dist(gen);
181 randCoords[i + 1] = y_dist(gen);
182 }
183
184 randCoords.insert(randCoords.end(), basicCoords.begin(), basicCoords.end());
185
186 auto origCoords = randCoords;
187
188 base::Timer correctedToRawTimer;
189 res = m.mapCorrectedToRaw(randCoords.data(), randCoords.size() / 2);
190 auto correctedToRawDurationMs = correctedToRawTimer.duration();
191 EXPECT_EQ(res, OK);
192
193 base::Timer rawToCorrectedTimer;
194 res = m.mapRawToCorrected(randCoords.data(), randCoords.size() / 2);
195 auto rawToCorrectedDurationMs = rawToCorrectedTimer.duration();
196 EXPECT_EQ(res, OK);
197
198 float correctedToRawDurationPerCoordUs =
199 (std::chrono::duration_cast<std::chrono::duration<double, std::micro>>(
200 correctedToRawDurationMs) / (randCoords.size() / 2) ).count();
201 float rawToCorrectedDurationPerCoordUs =
202 (std::chrono::duration_cast<std::chrono::duration<double, std::micro>>(
203 rawToCorrectedDurationMs) / (randCoords.size() / 2) ).count();
204
205 RecordProperty("CorrectedToRawDurationPerCoordUs",
206 base::StringPrintf("%f", correctedToRawDurationPerCoordUs));
207 RecordProperty("RawToCorrectedDurationPerCoordUs",
208 base::StringPrintf("%f", rawToCorrectedDurationPerCoordUs));
209
210 // Calculate mapping errors after round trip
211 float totalErrorSq = 0;
212 // Basic histogram; buckets go from [N to N+1)
213 std::array<int, maxAllowedPixelError * bucketsPerPixel> histogram = {0};
214 int outOfHistogram = 0;
215
216 for (size_t i = 0; i < randCoords.size(); i += 2) {
217 int xOrig = origCoords[i];
218 int yOrig = origCoords[i + 1];
219 int xMapped = randCoords[i];
220 int yMapped = randCoords[i + 1];
221
222 float errorSq = (xMapped - xOrig) * (xMapped - xOrig) +
223 (yMapped - yOrig) * (yMapped - yOrig);
224
225 EXPECT_LE(errorSq, maxAllowedPixelError * maxAllowedPixelError) << "( " <<
226 xOrig << "," << yOrig << ") -> (" << xMapped << "," << yMapped << ")";
227
228 // Note: Integer coordinates, so histogram will be clumpy; error distances can only be of
229 // form sqrt(X^2+Y^2) where X, Y are integers, so:
230 // 0, 1, sqrt(2), 2, sqrt(5), sqrt(8), 3, sqrt(10), sqrt(13), 4 ...
231 totalErrorSq += errorSq;
232 float errorDist = std::sqrt(errorSq);
233 if (errorDist < maxAllowedPixelError) {
234 int histBucket = static_cast<int>(errorDist * bucketsPerPixel); // rounds down
235 histogram[histBucket]++;
236 } else {
237 outOfHistogram++;
238 }
239 }
240
241 float rmsError = std::sqrt(totalErrorSq / randCoords.size());
242 RecordProperty("RmsError", base::StringPrintf("%f", rmsError));
243 for (size_t i = 0; i < histogram.size(); i++) {
244 std::string label = base::StringPrintf("HistogramBin[%f,%f)",
245 (float)i/bucketsPerPixel, (float)(i + 1)/bucketsPerPixel);
246 RecordProperty(label, histogram[i]);
247 }
248 RecordProperty("HistogramOutOfRange", outOfHistogram);
249 }
250
251 // Compare against values calculated by OpenCV
252 // undistortPoints() method, which is the same as mapRawToCorrected
253 // See script DistortionMapperComp.py
254 #include "DistortionMapperTest_OpenCvData.h"
255
TEST(DistortionMapperTest,CompareToOpenCV)256 TEST(DistortionMapperTest, CompareToOpenCV) {
257 status_t res;
258
259 float bigDistortion[] = {0.1, -0.003, 0.004, 0.02, 0.01};
260
261 // Expect to match within sqrt(2) radius pixels
262 const int32_t maxSqError = 2;
263
264 DistortionMapper m;
265 setupTestMapper(&m, bigDistortion);
266
267 using namespace openCvData;
268
269 res = m.mapRawToCorrected(rawCoords.data(), rawCoords.size() / 2);
270
271 for (size_t i = 0; i < rawCoords.size(); i+=2) {
272 int32_t dist = (rawCoords[i] - expCoords[i]) * (rawCoords[i] - expCoords[i]) +
273 (rawCoords[i + 1] - expCoords[i + 1]) * (rawCoords[i + 1] - expCoords[i + 1]);
274 EXPECT_LE(dist, maxSqError)
275 << "(" << rawCoords[i] << ", " << rawCoords[i + 1] << ") != ("
276 << expCoords[i] << ", " << expCoords[i + 1] << ")";
277 }
278 }
279