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