| /* |
| * Copyright (C) 2020 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| #define LOG_TAG "Camera3-RotCropMapper" |
| #define ATRACE_TAG ATRACE_TAG_CAMERA |
| //#define LOG_NDEBUG 0 |
| |
| #include <algorithm> |
| #include <cmath> |
| |
| #include "device3/RotateAndCropMapper.h" |
| |
| namespace android { |
| |
| namespace camera3 { |
| |
| void RotateAndCropMapper::initRemappedKeys() { |
| mRemappedKeys.insert( |
| kMeteringRegionsToCorrect.begin(), |
| kMeteringRegionsToCorrect.end()); |
| mRemappedKeys.insert( |
| kResultPointsToCorrectNoClamp.begin(), |
| kResultPointsToCorrectNoClamp.end()); |
| |
| mRemappedKeys.insert(ANDROID_SCALER_ROTATE_AND_CROP); |
| mRemappedKeys.insert(ANDROID_SCALER_CROP_REGION); |
| if (flags::concert_mode()) { |
| mRemappedKeys.insert(ANDROID_LOGICAL_MULTI_CAMERA_ACTIVE_PHYSICAL_SENSOR_CROP_REGION); |
| } |
| } |
| |
| bool RotateAndCropMapper::isNeeded(const CameraMetadata* deviceInfo) { |
| auto entry = deviceInfo->find(ANDROID_SCALER_AVAILABLE_ROTATE_AND_CROP_MODES); |
| for (size_t i = 0; i < entry.count; i++) { |
| if (entry.data.u8[i] == ANDROID_SCALER_ROTATE_AND_CROP_AUTO) return true; |
| } |
| return false; |
| } |
| |
| RotateAndCropMapper::RotateAndCropMapper(const CameraMetadata* deviceInfo) { |
| initRemappedKeys(); |
| |
| auto entry = deviceInfo->find(ANDROID_SENSOR_INFO_ACTIVE_ARRAY_SIZE); |
| if (entry.count != 4) return; |
| |
| mArrayWidth = entry.data.i32[2]; |
| mArrayHeight = entry.data.i32[3]; |
| mArrayAspect = static_cast<float>(mArrayWidth) / mArrayHeight; |
| mRotateAspect = 1.f/mArrayAspect; |
| } |
| |
| /** |
| * Adjust capture request when rotate and crop AUTO is enabled |
| */ |
| status_t RotateAndCropMapper::updateCaptureRequest(CameraMetadata *request) { |
| auto entry = request->find(ANDROID_SCALER_ROTATE_AND_CROP); |
| if (entry.count == 0) return OK; |
| uint8_t rotateMode = entry.data.u8[0]; |
| if (rotateMode == ANDROID_SCALER_ROTATE_AND_CROP_NONE) return OK; |
| |
| int32_t cx = 0; |
| int32_t cy = 0; |
| int32_t cw = mArrayWidth; |
| int32_t ch = mArrayHeight; |
| entry = request->find(ANDROID_SCALER_CROP_REGION); |
| if (entry.count == 4) { |
| cx = entry.data.i32[0]; |
| cy = entry.data.i32[1]; |
| cw = entry.data.i32[2]; |
| ch = entry.data.i32[3]; |
| } |
| |
| // User inputs are relative to the rotated-and-cropped view, so convert back |
| // to active array coordinates. To be more specific, the application is |
| // calculating coordinates based on the crop rectangle and the active array, |
| // even though the view the user sees is the cropped-and-rotated one. So we |
| // need to adjust the coordinates so that a point that would be on the |
| // top-left corner of the crop region is mapped to the top-left corner of |
| // the rotated-and-cropped fov within the crop region, and the same for the |
| // bottom-right corner. |
| // |
| // Since the zoom ratio control scales everything uniformly (so an app does |
| // not need to adjust anything if it wants to put a metering region on the |
| // top-left quadrant of the preview FOV, when changing zoomRatio), it does |
| // not need to be factored into this calculation at all. |
| // |
| // ->+x active array aw |
| // |+--------------------------------------------------------------------+ |
| // v| | |
| // +y| a 1 cw 2 b | |
| // | +=========*HHHHHHHHHHHHHHH*===========+ | |
| // | I H rw H I | |
| // | I H H I | |
| // | I H H I | |
| //ah | ch I H rh H I crop region | |
| // | I H H I | |
| // | I H H I | |
| // | I H rotate region H I | |
| // | +=========*HHHHHHHHHHHHHHH*===========+ | |
| // | d 4 3 c | |
| // | | |
| // +--------------------------------------------------------------------+ |
| // |
| // aw , ah = active array width,height |
| // cw , ch = crop region width,height |
| // rw , rh = rotated-and-cropped region width,height |
| // aw / ah = array aspect = rh / rw = 1 / rotated aspect |
| // Coordinate mappings: |
| // ROTATE_AND_CROP_90: point a -> point 2 |
| // point c -> point 4 = +x -> +y, +y -> -x |
| // ROTATE_AND_CROP_180: point a -> point c |
| // point c -> point a = +x -> -x, +y -> -y |
| // ROTATE_AND_CROP_270: point a -> point 4 |
| // point c -> point 2 = +x -> -y, +y -> +x |
| |
| float cropAspect = static_cast<float>(cw) / ch; |
| float transformMat[4] = {0, 0, |
| 0, 0}; |
| float xShift = 0; |
| float yShift = 0; |
| |
| if (rotateMode == ANDROID_SCALER_ROTATE_AND_CROP_180) { |
| transformMat[0] = -1; |
| transformMat[3] = -1; |
| xShift = cw; |
| yShift = ch; |
| } else { |
| float rw = cropAspect > mRotateAspect ? |
| ch * mRotateAspect : // pillarbox, not full width |
| cw; // letterbox or 1:1, full width |
| float rh = cropAspect >= mRotateAspect ? |
| ch : // pillarbox or 1:1, full height |
| cw / mRotateAspect; // letterbox, not full height |
| switch (rotateMode) { |
| case ANDROID_SCALER_ROTATE_AND_CROP_270: |
| transformMat[1] = -rw / ch; // +y -> -x |
| transformMat[2] = rh / cw; // +x -> +y |
| xShift = (cw + rw) / 2; // left edge of crop to right edge of rotated |
| yShift = (ch - rh) / 2; // top edge of crop to top edge of rotated |
| break; |
| case ANDROID_SCALER_ROTATE_AND_CROP_90: |
| transformMat[1] = rw / ch; // +y -> +x |
| transformMat[2] = -rh / cw; // +x -> -y |
| xShift = (cw - rw) / 2; // left edge of crop to left edge of rotated |
| yShift = (ch + rh) / 2; // top edge of crop to bottom edge of rotated |
| break; |
| default: |
| ALOGE("%s: Unexpected rotate mode: %d", __FUNCTION__, rotateMode); |
| return BAD_VALUE; |
| } |
| } |
| |
| for (auto regionTag : kMeteringRegionsToCorrect) { |
| entry = request->find(regionTag); |
| for (size_t i = 0; i < entry.count; i += 5) { |
| int32_t weight = entry.data.i32[i + 4]; |
| if (weight == 0) { |
| continue; |
| } |
| transformPoints(entry.data.i32 + i, 2, transformMat, xShift, yShift, cx, cy); |
| swapRectToMinFirst(entry.data.i32 + i); |
| } |
| } |
| |
| return OK; |
| } |
| |
| /** |
| * Adjust capture result when rotate and crop AUTO is enabled |
| */ |
| status_t RotateAndCropMapper::updateCaptureResult(CameraMetadata *result) { |
| auto entry = result->find(ANDROID_SCALER_ROTATE_AND_CROP); |
| if (entry.count == 0) return OK; |
| uint8_t rotateMode = entry.data.u8[0]; |
| if (rotateMode == ANDROID_SCALER_ROTATE_AND_CROP_NONE) return OK; |
| |
| int32_t cx = 0; |
| int32_t cy = 0; |
| int32_t cw = mArrayWidth; |
| int32_t ch = mArrayHeight; |
| entry = result->find(ANDROID_SCALER_CROP_REGION); |
| if (entry.count == 4) { |
| cx = entry.data.i32[0]; |
| cy = entry.data.i32[1]; |
| cw = entry.data.i32[2]; |
| ch = entry.data.i32[3]; |
| } |
| |
| // HAL inputs are relative to the full active array, so convert back to |
| // rotated-and-cropped coordinates for apps. To be more specific, the |
| // application is calculating coordinates based on the crop rectangle and |
| // the active array, even though the view the user sees is the |
| // cropped-and-rotated one. So we need to adjust the coordinates so that a |
| // point that would be on the top-left corner of the rotate-and-cropped |
| // region is mapped to the top-left corner of the crop region, and the same |
| // for the bottom-right corner. |
| // |
| // Since the zoom ratio control scales everything uniformly (so an app does |
| // not need to adjust anything if it wants to put a metering region on the |
| // top-left quadrant of the preview FOV, when changing zoomRatio), it does |
| // not need to be factored into this calculation at all. |
| // |
| // Also note that round-tripping between original request and final result |
| // fields can't be perfect, since the intermediate values have to be |
| // integers on a smaller range than the original crop region range. That |
| // means that multiple input values map to a single output value in |
| // adjusting a request, so when adjusting a result, the original answer may |
| // not be obtainable. Given that aspect ratios are rarely > 16/9, the |
| // round-trip values should generally only be off by 1 at most. |
| // |
| // ->+x active array aw |
| // |+--------------------------------------------------------------------+ |
| // v| | |
| // +y| a 1 cw 2 b | |
| // | +=========*HHHHHHHHHHHHHHH*===========+ | |
| // | I H rw H I | |
| // | I H H I | |
| // | I H H I | |
| //ah | ch I H rh H I crop region | |
| // | I H H I | |
| // | I H H I | |
| // | I H rotate region H I | |
| // | +=========*HHHHHHHHHHHHHHH*===========+ | |
| // | d 4 3 c | |
| // | | |
| // +--------------------------------------------------------------------+ |
| // |
| // aw , ah = active array width,height |
| // cw , ch = crop region width,height |
| // rw , rh = rotated-and-cropped region width,height |
| // aw / ah = array aspect = rh / rw = 1 / rotated aspect |
| // Coordinate mappings: |
| // ROTATE_AND_CROP_90: point 2 -> point a |
| // point 4 -> point c = +x -> -y, +y -> +x |
| // ROTATE_AND_CROP_180: point c -> point a |
| // point a -> point c = +x -> -x, +y -> -y |
| // ROTATE_AND_CROP_270: point 4 -> point a |
| // point 2 -> point c = +x -> +y, +y -> -x |
| |
| float cropAspect = static_cast<float>(cw) / ch; |
| float transformMat[4] = {0, 0, |
| 0, 0}; |
| float xShift = 0; |
| float yShift = 0; |
| float rx = 0; // top-left corner of rotated region |
| float ry = 0; |
| if (rotateMode == ANDROID_SCALER_ROTATE_AND_CROP_180) { |
| transformMat[0] = -1; |
| transformMat[3] = -1; |
| xShift = cw; |
| yShift = ch; |
| rx = cx; |
| ry = cy; |
| } else { |
| float rw = cropAspect > mRotateAspect ? |
| ch * mRotateAspect : // pillarbox, not full width |
| cw; // letterbox or 1:1, full width |
| float rh = cropAspect >= mRotateAspect ? |
| ch : // pillarbox or 1:1, full height |
| cw / mRotateAspect; // letterbox, not full height |
| rx = cx + (cw - rw) / 2; |
| ry = cy + (ch - rh) / 2; |
| switch (rotateMode) { |
| case ANDROID_SCALER_ROTATE_AND_CROP_270: |
| transformMat[1] = ch / rw; // +y -> +x |
| transformMat[2] = -cw / rh; // +x -> -y |
| xShift = -(cw - rw) / 2; // left edge of rotated to left edge of cropped |
| yShift = ry - cy + ch; // top edge of rotated to bottom edge of cropped |
| break; |
| case ANDROID_SCALER_ROTATE_AND_CROP_90: |
| transformMat[1] = -ch / rw; // +y -> -x |
| transformMat[2] = cw / rh; // +x -> +y |
| xShift = (cw + rw) / 2; // left edge of rotated to left edge of cropped |
| yShift = (ch - rh) / 2; // top edge of rotated to bottom edge of cropped |
| break; |
| default: |
| ALOGE("%s: Unexpected rotate mode: %d", __FUNCTION__, rotateMode); |
| return BAD_VALUE; |
| } |
| } |
| |
| for (auto regionTag : kMeteringRegionsToCorrect) { |
| entry = result->find(regionTag); |
| for (size_t i = 0; i < entry.count; i += 5) { |
| int32_t weight = entry.data.i32[i + 4]; |
| if (weight == 0) { |
| continue; |
| } |
| transformPoints(entry.data.i32 + i, 2, transformMat, xShift, yShift, rx, ry); |
| swapRectToMinFirst(entry.data.i32 + i); |
| } |
| } |
| |
| for (auto pointsTag: kResultPointsToCorrectNoClamp) { |
| entry = result->find(pointsTag); |
| transformPoints(entry.data.i32, entry.count / 2, transformMat, xShift, yShift, rx, ry); |
| if (pointsTag == ANDROID_STATISTICS_FACE_RECTANGLES) { |
| for (size_t i = 0; i < entry.count; i += 4) { |
| swapRectToMinFirst(entry.data.i32 + i); |
| } |
| } |
| } |
| |
| return OK; |
| } |
| |
| void RotateAndCropMapper::transformPoints(int32_t* pts, size_t count, float transformMat[4], |
| float xShift, float yShift, float ox, float oy) { |
| for (size_t i = 0; i < count * 2; i += 2) { |
| float x0 = pts[i] - ox; |
| float y0 = pts[i + 1] - oy; |
| int32_t nx = std::round(transformMat[0] * x0 + transformMat[1] * y0 + xShift + ox); |
| int32_t ny = std::round(transformMat[2] * x0 + transformMat[3] * y0 + yShift + oy); |
| |
| pts[i] = std::min(std::max(nx, 0), mArrayWidth); |
| pts[i + 1] = std::min(std::max(ny, 0), mArrayHeight); |
| } |
| } |
| |
| void RotateAndCropMapper::swapRectToMinFirst(int32_t* rect) { |
| if (rect[0] > rect[2]) { |
| auto tmp = rect[0]; |
| rect[0] = rect[2]; |
| rect[2] = tmp; |
| } |
| if (rect[1] > rect[3]) { |
| auto tmp = rect[1]; |
| rect[1] = rect[3]; |
| rect[3] = tmp; |
| } |
| } |
| |
| } // namespace camera3 |
| |
| } // namespace android |