1 /*
2 * Copyright (C) 2020 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_TAG "Camera3-RotCropMapper"
18 #define ATRACE_TAG ATRACE_TAG_CAMERA
19 //#define LOG_NDEBUG 0
20
21 #include <algorithm>
22 #include <cmath>
23
24 #include "device3/RotateAndCropMapper.h"
25
26 namespace android {
27
28 namespace camera3 {
29
initRemappedKeys()30 void RotateAndCropMapper::initRemappedKeys() {
31 mRemappedKeys.insert(
32 kMeteringRegionsToCorrect.begin(),
33 kMeteringRegionsToCorrect.end());
34 mRemappedKeys.insert(
35 kResultPointsToCorrectNoClamp.begin(),
36 kResultPointsToCorrectNoClamp.end());
37
38 mRemappedKeys.insert(ANDROID_SCALER_ROTATE_AND_CROP);
39 mRemappedKeys.insert(ANDROID_SCALER_CROP_REGION);
40 if (flags::concert_mode()) {
41 mRemappedKeys.insert(ANDROID_LOGICAL_MULTI_CAMERA_ACTIVE_PHYSICAL_SENSOR_CROP_REGION);
42 }
43 }
44
isNeeded(const CameraMetadata * deviceInfo)45 bool RotateAndCropMapper::isNeeded(const CameraMetadata* deviceInfo) {
46 auto entry = deviceInfo->find(ANDROID_SCALER_AVAILABLE_ROTATE_AND_CROP_MODES);
47 for (size_t i = 0; i < entry.count; i++) {
48 if (entry.data.u8[i] == ANDROID_SCALER_ROTATE_AND_CROP_AUTO) return true;
49 }
50 return false;
51 }
52
RotateAndCropMapper(const CameraMetadata * deviceInfo)53 RotateAndCropMapper::RotateAndCropMapper(const CameraMetadata* deviceInfo) {
54 initRemappedKeys();
55
56 auto entry = deviceInfo->find(ANDROID_SENSOR_INFO_ACTIVE_ARRAY_SIZE);
57 if (entry.count != 4) return;
58
59 mArrayWidth = entry.data.i32[2];
60 mArrayHeight = entry.data.i32[3];
61 mArrayAspect = static_cast<float>(mArrayWidth) / mArrayHeight;
62 mRotateAspect = 1.f/mArrayAspect;
63 }
64
65 /**
66 * Adjust capture request when rotate and crop AUTO is enabled
67 */
updateCaptureRequest(CameraMetadata * request)68 status_t RotateAndCropMapper::updateCaptureRequest(CameraMetadata *request) {
69 auto entry = request->find(ANDROID_SCALER_ROTATE_AND_CROP);
70 if (entry.count == 0) return OK;
71 uint8_t rotateMode = entry.data.u8[0];
72 if (rotateMode == ANDROID_SCALER_ROTATE_AND_CROP_NONE) return OK;
73
74 int32_t cx = 0;
75 int32_t cy = 0;
76 int32_t cw = mArrayWidth;
77 int32_t ch = mArrayHeight;
78 entry = request->find(ANDROID_SCALER_CROP_REGION);
79 if (entry.count == 4) {
80 cx = entry.data.i32[0];
81 cy = entry.data.i32[1];
82 cw = entry.data.i32[2];
83 ch = entry.data.i32[3];
84 }
85
86 // User inputs are relative to the rotated-and-cropped view, so convert back
87 // to active array coordinates. To be more specific, the application is
88 // calculating coordinates based on the crop rectangle and the active array,
89 // even though the view the user sees is the cropped-and-rotated one. So we
90 // need to adjust the coordinates so that a point that would be on the
91 // top-left corner of the crop region is mapped to the top-left corner of
92 // the rotated-and-cropped fov within the crop region, and the same for the
93 // bottom-right corner.
94 //
95 // Since the zoom ratio control scales everything uniformly (so an app does
96 // not need to adjust anything if it wants to put a metering region on the
97 // top-left quadrant of the preview FOV, when changing zoomRatio), it does
98 // not need to be factored into this calculation at all.
99 //
100 // ->+x active array aw
101 // |+--------------------------------------------------------------------+
102 // v| |
103 // +y| a 1 cw 2 b |
104 // | +=========*HHHHHHHHHHHHHHH*===========+ |
105 // | I H rw H I |
106 // | I H H I |
107 // | I H H I |
108 //ah | ch I H rh H I crop region |
109 // | I H H I |
110 // | I H H I |
111 // | I H rotate region H I |
112 // | +=========*HHHHHHHHHHHHHHH*===========+ |
113 // | d 4 3 c |
114 // | |
115 // +--------------------------------------------------------------------+
116 //
117 // aw , ah = active array width,height
118 // cw , ch = crop region width,height
119 // rw , rh = rotated-and-cropped region width,height
120 // aw / ah = array aspect = rh / rw = 1 / rotated aspect
121 // Coordinate mappings:
122 // ROTATE_AND_CROP_90: point a -> point 2
123 // point c -> point 4 = +x -> +y, +y -> -x
124 // ROTATE_AND_CROP_180: point a -> point c
125 // point c -> point a = +x -> -x, +y -> -y
126 // ROTATE_AND_CROP_270: point a -> point 4
127 // point c -> point 2 = +x -> -y, +y -> +x
128
129 float cropAspect = static_cast<float>(cw) / ch;
130 float transformMat[4] = {0, 0,
131 0, 0};
132 float xShift = 0;
133 float yShift = 0;
134
135 if (rotateMode == ANDROID_SCALER_ROTATE_AND_CROP_180) {
136 transformMat[0] = -1;
137 transformMat[3] = -1;
138 xShift = cw;
139 yShift = ch;
140 } else {
141 float rw = cropAspect > mRotateAspect ?
142 ch * mRotateAspect : // pillarbox, not full width
143 cw; // letterbox or 1:1, full width
144 float rh = cropAspect >= mRotateAspect ?
145 ch : // pillarbox or 1:1, full height
146 cw / mRotateAspect; // letterbox, not full height
147 switch (rotateMode) {
148 case ANDROID_SCALER_ROTATE_AND_CROP_270:
149 transformMat[1] = -rw / ch; // +y -> -x
150 transformMat[2] = rh / cw; // +x -> +y
151 xShift = (cw + rw) / 2; // left edge of crop to right edge of rotated
152 yShift = (ch - rh) / 2; // top edge of crop to top edge of rotated
153 break;
154 case ANDROID_SCALER_ROTATE_AND_CROP_90:
155 transformMat[1] = rw / ch; // +y -> +x
156 transformMat[2] = -rh / cw; // +x -> -y
157 xShift = (cw - rw) / 2; // left edge of crop to left edge of rotated
158 yShift = (ch + rh) / 2; // top edge of crop to bottom edge of rotated
159 break;
160 default:
161 ALOGE("%s: Unexpected rotate mode: %d", __FUNCTION__, rotateMode);
162 return BAD_VALUE;
163 }
164 }
165
166 for (auto regionTag : kMeteringRegionsToCorrect) {
167 entry = request->find(regionTag);
168 for (size_t i = 0; i < entry.count; i += 5) {
169 int32_t weight = entry.data.i32[i + 4];
170 if (weight == 0) {
171 continue;
172 }
173 transformPoints(entry.data.i32 + i, 2, transformMat, xShift, yShift, cx, cy);
174 swapRectToMinFirst(entry.data.i32 + i);
175 }
176 }
177
178 return OK;
179 }
180
181 /**
182 * Adjust capture result when rotate and crop AUTO is enabled
183 */
updateCaptureResult(CameraMetadata * result)184 status_t RotateAndCropMapper::updateCaptureResult(CameraMetadata *result) {
185 auto entry = result->find(ANDROID_SCALER_ROTATE_AND_CROP);
186 if (entry.count == 0) return OK;
187 uint8_t rotateMode = entry.data.u8[0];
188 if (rotateMode == ANDROID_SCALER_ROTATE_AND_CROP_NONE) return OK;
189
190 int32_t cx = 0;
191 int32_t cy = 0;
192 int32_t cw = mArrayWidth;
193 int32_t ch = mArrayHeight;
194 entry = result->find(ANDROID_SCALER_CROP_REGION);
195 if (entry.count == 4) {
196 cx = entry.data.i32[0];
197 cy = entry.data.i32[1];
198 cw = entry.data.i32[2];
199 ch = entry.data.i32[3];
200 }
201
202 // HAL inputs are relative to the full active array, so convert back to
203 // rotated-and-cropped coordinates for apps. To be more specific, the
204 // application is calculating coordinates based on the crop rectangle and
205 // the active array, even though the view the user sees is the
206 // cropped-and-rotated one. So we need to adjust the coordinates so that a
207 // point that would be on the top-left corner of the rotate-and-cropped
208 // region is mapped to the top-left corner of the crop region, and the same
209 // for the bottom-right corner.
210 //
211 // Since the zoom ratio control scales everything uniformly (so an app does
212 // not need to adjust anything if it wants to put a metering region on the
213 // top-left quadrant of the preview FOV, when changing zoomRatio), it does
214 // not need to be factored into this calculation at all.
215 //
216 // Also note that round-tripping between original request and final result
217 // fields can't be perfect, since the intermediate values have to be
218 // integers on a smaller range than the original crop region range. That
219 // means that multiple input values map to a single output value in
220 // adjusting a request, so when adjusting a result, the original answer may
221 // not be obtainable. Given that aspect ratios are rarely > 16/9, the
222 // round-trip values should generally only be off by 1 at most.
223 //
224 // ->+x active array aw
225 // |+--------------------------------------------------------------------+
226 // v| |
227 // +y| a 1 cw 2 b |
228 // | +=========*HHHHHHHHHHHHHHH*===========+ |
229 // | I H rw H I |
230 // | I H H I |
231 // | I H H I |
232 //ah | ch I H rh H I crop region |
233 // | I H H I |
234 // | I H H I |
235 // | I H rotate region H I |
236 // | +=========*HHHHHHHHHHHHHHH*===========+ |
237 // | d 4 3 c |
238 // | |
239 // +--------------------------------------------------------------------+
240 //
241 // aw , ah = active array width,height
242 // cw , ch = crop region width,height
243 // rw , rh = rotated-and-cropped region width,height
244 // aw / ah = array aspect = rh / rw = 1 / rotated aspect
245 // Coordinate mappings:
246 // ROTATE_AND_CROP_90: point 2 -> point a
247 // point 4 -> point c = +x -> -y, +y -> +x
248 // ROTATE_AND_CROP_180: point c -> point a
249 // point a -> point c = +x -> -x, +y -> -y
250 // ROTATE_AND_CROP_270: point 4 -> point a
251 // point 2 -> point c = +x -> +y, +y -> -x
252
253 float cropAspect = static_cast<float>(cw) / ch;
254 float transformMat[4] = {0, 0,
255 0, 0};
256 float xShift = 0;
257 float yShift = 0;
258 float rx = 0; // top-left corner of rotated region
259 float ry = 0;
260 if (rotateMode == ANDROID_SCALER_ROTATE_AND_CROP_180) {
261 transformMat[0] = -1;
262 transformMat[3] = -1;
263 xShift = cw;
264 yShift = ch;
265 rx = cx;
266 ry = cy;
267 } else {
268 float rw = cropAspect > mRotateAspect ?
269 ch * mRotateAspect : // pillarbox, not full width
270 cw; // letterbox or 1:1, full width
271 float rh = cropAspect >= mRotateAspect ?
272 ch : // pillarbox or 1:1, full height
273 cw / mRotateAspect; // letterbox, not full height
274 rx = cx + (cw - rw) / 2;
275 ry = cy + (ch - rh) / 2;
276 switch (rotateMode) {
277 case ANDROID_SCALER_ROTATE_AND_CROP_270:
278 transformMat[1] = ch / rw; // +y -> +x
279 transformMat[2] = -cw / rh; // +x -> -y
280 xShift = -(cw - rw) / 2; // left edge of rotated to left edge of cropped
281 yShift = ry - cy + ch; // top edge of rotated to bottom edge of cropped
282 break;
283 case ANDROID_SCALER_ROTATE_AND_CROP_90:
284 transformMat[1] = -ch / rw; // +y -> -x
285 transformMat[2] = cw / rh; // +x -> +y
286 xShift = (cw + rw) / 2; // left edge of rotated to left edge of cropped
287 yShift = (ch - rh) / 2; // top edge of rotated to bottom edge of cropped
288 break;
289 default:
290 ALOGE("%s: Unexpected rotate mode: %d", __FUNCTION__, rotateMode);
291 return BAD_VALUE;
292 }
293 }
294
295 for (auto regionTag : kMeteringRegionsToCorrect) {
296 entry = result->find(regionTag);
297 for (size_t i = 0; i < entry.count; i += 5) {
298 int32_t weight = entry.data.i32[i + 4];
299 if (weight == 0) {
300 continue;
301 }
302 transformPoints(entry.data.i32 + i, 2, transformMat, xShift, yShift, rx, ry);
303 swapRectToMinFirst(entry.data.i32 + i);
304 }
305 }
306
307 for (auto pointsTag: kResultPointsToCorrectNoClamp) {
308 entry = result->find(pointsTag);
309 transformPoints(entry.data.i32, entry.count / 2, transformMat, xShift, yShift, rx, ry);
310 if (pointsTag == ANDROID_STATISTICS_FACE_RECTANGLES) {
311 for (size_t i = 0; i < entry.count; i += 4) {
312 swapRectToMinFirst(entry.data.i32 + i);
313 }
314 }
315 }
316
317 return OK;
318 }
319
transformPoints(int32_t * pts,size_t count,float transformMat[4],float xShift,float yShift,float ox,float oy)320 void RotateAndCropMapper::transformPoints(int32_t* pts, size_t count, float transformMat[4],
321 float xShift, float yShift, float ox, float oy) {
322 for (size_t i = 0; i < count * 2; i += 2) {
323 float x0 = pts[i] - ox;
324 float y0 = pts[i + 1] - oy;
325 int32_t nx = std::round(transformMat[0] * x0 + transformMat[1] * y0 + xShift + ox);
326 int32_t ny = std::round(transformMat[2] * x0 + transformMat[3] * y0 + yShift + oy);
327
328 pts[i] = std::min(std::max(nx, 0), mArrayWidth);
329 pts[i + 1] = std::min(std::max(ny, 0), mArrayHeight);
330 }
331 }
332
swapRectToMinFirst(int32_t * rect)333 void RotateAndCropMapper::swapRectToMinFirst(int32_t* rect) {
334 if (rect[0] > rect[2]) {
335 auto tmp = rect[0];
336 rect[0] = rect[2];
337 rect[2] = tmp;
338 }
339 if (rect[1] > rect[3]) {
340 auto tmp = rect[1];
341 rect[1] = rect[3];
342 rect[3] = tmp;
343 }
344 }
345
346 } // namespace camera3
347
348 } // namespace android
349